From c4fc77bc58af2f7a66fc4bef2edd8d4358a63579 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Mon, 13 Jan 2025 22:34:17 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=E3=82=8F=E3=81=AA=E3=81=AE?= =?UTF-8?q?=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=AE=E7=A8=AE=E9=A1=9E?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../images/icons/{trap.svg => trap-box.svg} | 0 public/static/images/icons/trap-gun.svg | 141 +++++ public/static/images/icons/trap-tie.svg | 484 ++++++++++++++++++ .../organisms/infoTypeSelector/index.tsx | 2 +- src/components/organisms/mapBase/base.tsx | 26 +- .../organisms/selectionMap/base.tsx | 26 +- src/utils/constants.ts | 2 +- 7 files changed, 675 insertions(+), 6 deletions(-) rename public/static/images/icons/{trap.svg => trap-box.svg} (100%) create mode 100644 public/static/images/icons/trap-gun.svg create mode 100644 public/static/images/icons/trap-tie.svg diff --git a/public/static/images/icons/trap.svg b/public/static/images/icons/trap-box.svg similarity index 100% rename from public/static/images/icons/trap.svg rename to public/static/images/icons/trap-box.svg diff --git a/public/static/images/icons/trap-gun.svg b/public/static/images/icons/trap-gun.svg new file mode 100644 index 00000000..d0d39e22 --- /dev/null +++ b/public/static/images/icons/trap-gun.svg @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="100mm" + height="100mm" + viewBox="0 0 100 100" + version="1.1" + id="svg1" + xml:space="preserve" + inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)" + sodipodi:docname="猟銃_旧アイコン準拠.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="mm" + inkscape:zoom="1.0444209" + inkscape:cx="220.21773" + inkscape:cy="215.90912" + inkscape:window-width="1800" + inkscape:window-height="1106" + inkscape:window-x="-11" + inkscape:window-y="-11" + inkscape:window-maximized="1" + inkscape:current-layer="layer2" /><defs + id="defs1"><inkscape:path-effect + effect="bspline" + id="path-effect11" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /><inkscape:path-effect + effect="bspline" + id="path-effect9" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /></defs><g + inkscape:label="旧アイコン" + inkscape:groupmode="layer" + id="layer1" + style="display:none"><image + width="95" + height="95" + preserveAspectRatio="none" + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAyxpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6 eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1 NDkxMSwgMjAxMy8xMC8yOS0xMTo0NzoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIEVsZW1lbnRzIDEzLjAgKFdpbmRvd3MpIiB4bXBN TTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjM4QzM1MDc2M0IxMTExRUU5REM1OTVBRTc4NUNBMjYzIiB4 bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjM4QzM1MDc3M0IxMTExRUU5REM1OTVBRTc4NUNBMjYz Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MzhDMzUwNzQz QjExMTFFRTlEQzU5NUFFNzg1Q0EyNjMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MzhDMzUw NzUzQjExMTFFRTlEQzU5NUFFNzg1Q0EyNjMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJE Rj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4f8hIaAAABgFBMVEWNDw/HiIiUHh6Z KSm7b2+KCQmgNzeXIyOvVVWFAACHAwP///+LCwv//v6IBQX9+/v37e2GAQGIBASLCgqtU1PMlJTd t7eDAACJBwf+/PyLDAy1Y2PeubmnRUXy4+OqTEz48PCvVlaSGhr+/f25bGzu29v9+vr79/f06Oiw WFjGhob37u6WISGcLy+AAACGAAD16em1ZGT+/v7Ae3ucLi6yXFyJBgaxW1uTHR369fXlx8fFhIS7 cHDas7PTo6PKj4+HAgLbsrLOmJjOmZmzX1+IBgapSUmdLy+YJia6bW2PExObLS28cnKJCAjLkZF/ AACjQEDQnJz16ur37+/06en89/fx4eHIjIzJjo748fG+dnasUVHy5OSuU1PCfn7SoaGcLS2oSkqr T0+dMTGfNTXasrLNmJjq1NSKCgr8+PjCgIDDgIDDgYGGAgLIioriwcHmycnmysq1YmLlxsbAenqh OTnVpqbVp6e/d3e3aGiSGBiyXV3Cf3/Eg4OMDQ3///8F+8JAAAAAgHRSTlP///////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// ADgFS2cAAAEvSURBVHjaYqgnABhGFWAqELPVCpTjV1ZWT68psw/BooAxLkW8jjnBREdCT0VWFYuC 8JxScV8dZuYqIQwFvCDALZZa66wYxi9Znm1tl6cBVSASwZgkL2JoKC0tXVTtUs2iq8liwxbFDQRQ BTyaiQ51PFxwwMrHxSUhqKgO90WufAAnHx+fEF8dMtB3N4CZkCbm6CcqKupkKigFlJDi4eEBqxDU CUXzRaY5kwUTE5OSkrAvlxlQrZBMfb2AJDeSN3mDBbQFBAyC5NxKOHwz6ngYfCq9ko1jsAR1lkZB MZtEHY8Qn7ArpzcWBfFaCpbR4lxmIKew6mJRoObmwe4pY2WqD3JrBbbYlFSINKqv5+fgBCrIxxPd /LGcwgza+NIDv4i/iRHeBKNmXDhMkj1AgAEAekWuTpyNG5UAAAAASUVORK5CYII= " + id="image1" + x="2.5" + y="2.5" /></g><g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Layer1"><rect + style="display:inline;fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.910314;stroke-opacity:1" + id="rect1" + width="54.089687" + height="9.0896854" + x="8.4551573" + y="42.455162" /><path + id="rect2" + style="fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:1.06422;stroke-opacity:1" + d="m 21.434686,42.532108 13.928167,13.935784 h 39.134275 v -5.21186 l -8.719381,-8.723924 z" /><path + id="rect3" + style="fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.978054;stroke-opacity:1" + d="M 60.862229,53.699491 72.033396,64.225749 H 94.610238 V 59.003827 L 88.980923,53.699491 Z" /><rect + style="display:inline;fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.24832;stroke-opacity:1" + id="rect4" + width="3.7516797" + height="9.7516804" + x="3.1241601" + y="42.124161" /><rect + style="display:inline;fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.279656;stroke-opacity:1" + id="rect5" + width="4.7736239" + height="9.7203436" + x="42.139828" + y="30.139828" /><rect + style="display:inline;fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.278157;stroke-opacity:1" + id="rect6" + width="4.7218432" + height="9.7218428" + x="68.139084" + y="30.139078" /><rect + style="display:inline;fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.553896;stroke-opacity:1" + id="rect7" + width="24.446104" + height="7.4461036" + x="45.100552" + y="31.277338" /><rect + style="display:inline;fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.173905;stroke-opacity:1" + id="rect8" + width="1.826095" + height="9.8260956" + x="39.086948" + y="30.086952" /><rect + style="display:inline;fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:0.173905;stroke-opacity:1" + id="rect9" + width="1.826095" + height="9.8260956" + x="74.086952" + y="30.086952" /><path + style="fill:none;fill-opacity:1;stroke:#8c0d0d;stroke-width:2.5;stroke-dasharray:none;stroke-opacity:1" + d="m 57.505949,54.085992 c -0.08444,3.039962 -0.168887,6.079924 0.316673,8.064353 0.48556,1.984428 1.541082,2.913287 3.335528,3.419948 1.794447,0.506661 4.327698,0.591103 5.953241,0.591102 1.625544,0 2.343297,-0.08444 2.702181,-0.823336 0.358885,-0.738893 0.358885,-2.132181 0.358885,-3.525498" + id="path9" + inkscape:path-effect="#path-effect9" + inkscape:original-d="m 57.505949,54.085992 c -0.08445,3.039962 -0.168887,6.079924 -0.253331,9.119886 1.055563,0.928896 2.111087,1.857753 3.166628,2.786631 2.533351,0.08445 5.066602,0.168886 7.599905,0.253331 0.717783,-0.08445 1.435537,-0.168886 2.153306,-0.253331 0,-1.393343 0,-2.786631 0,-4.179948" + transform="matrix(1,0,0.08738187,1,-7.4234852,0.21669041)" /><path + id="rect10" + style="fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:2.42458;stroke-dasharray:none;stroke-opacity:1" + d="m 76.83629,56.723357 v 7.030062 l 14.799102,14.375871 2.285647,-10e-7 V 60.315906 l -3.698484,-3.592548 z" /><rect + style="fill:#8c0d0d;fill-opacity:1;stroke:#8c0d0d;stroke-width:2.5;stroke-dasharray:none;stroke-opacity:1" + id="rect11" + width="10.259872" + height="4.0532827" + x="68.16832" + y="61.075081" /><path + style="fill:none;fill-opacity:1;stroke:#8c0d0d;stroke-width:2;stroke-dasharray:none;stroke-opacity:1" + d="m 62.825883,56.112632 c 0.08444,1.984419 0.168887,3.968839 -0.07389,5.172162 -0.242781,1.203322 -0.812763,1.625531 -1.382757,2.047748" + id="path11" + inkscape:path-effect="#path-effect11" + inkscape:original-d="m 62.825883,56.112632 c 0.08444,1.984419 0.168886,3.968839 0.253331,5.953259 -0.570005,0.422225 -1.139987,0.844434 -1.709981,1.266651" + transform="translate(-0.2758365,-0.29834278)" /></g></svg> diff --git a/public/static/images/icons/trap-tie.svg b/public/static/images/icons/trap-tie.svg new file mode 100644 index 00000000..088f1555 --- /dev/null +++ b/public/static/images/icons/trap-tie.svg @@ -0,0 +1,484 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="100mm" + height="100mm" + viewBox="0 0 100 100" + version="1.1" + id="svg1" + inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)" + sodipodi:docname="くくり罠_旧アイコン準拠.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="mm" + inkscape:zoom="1.1019522" + inkscape:cx="284.04135" + inkscape:cy="262.71556" + inkscape:window-width="1800" + inkscape:window-height="1106" + inkscape:window-x="-11" + inkscape:window-y="-11" + inkscape:window-maximized="1" + inkscape:current-layer="layer7" /> + <defs + id="defs1"> + <inkscape:path-effect + effect="bspline" + id="path-effect35" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect33" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect32" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect31" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect29" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect20" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect19" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect44" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect42" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect40" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect38" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <linearGradient + id="swatch32" + inkscape:swatch="solid"> + <stop + style="stop-color:#ff0909;stop-opacity:1;" + offset="0" + id="stop32" /> + </linearGradient> + <inkscape:path-effect + effect="bspline" + id="path-effect24" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect23" + is_visible="true" + lpeversion="1.3" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" + uniform="false" /> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 50 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="100 : 50 : 1" + inkscape:persp3d-origin="50 : 33.333333 : 1" + id="perspective10" /> + </defs> + <g + inkscape:groupmode="layer" + id="layer5" + inkscape:label="地面" + style="display:inline"> + <ellipse + style="fill:#000000;fill-opacity:0.199588;stroke:none;stroke-width:1.30197" + id="path1" + cx="50.169777" + cy="73.463669" + rx="46.774193" + ry="21.103394" /> + </g> + <g + inkscape:groupmode="layer" + id="layer6" + inkscape:label="色" + style="display:inline;fill:#333333"> + <path + style="fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:7.55905522;stroke-dasharray:none;stroke-opacity:1" + d="M 160.10053,337.41866 C 88.332585,334.82748 30.267018,307.26302 22.436863,272.06798 c -0.847039,-3.80729 -0.979445,-8.49409 -0.807487,-28.58278 0.225278,-26.31768 0.108565,-25.42063 4.474238,-34.38874 3.974013,-8.16354 13.294175,-17.91307 23.947856,-25.05109 42.389257,-28.40102 116.71332,-38.01388 181.91784,-23.52871 4.05866,0.90163 7.64378,1.80097 7.96694,1.99853 0.32316,0.19757 -4.19,2.75132 -10.02926,5.67499 l -10.61683,5.31578 -9.54214,-1.22627 c -55.53266,-7.13654 -116.884821,1.63717 -146.309603,20.92312 -5.736442,3.75985 -12.162884,10.46161 -14.198479,14.80676 -1.509637,3.22245 -1.753488,4.55466 -1.742898,9.52182 0.01085,5.08824 0.229611,6.19084 1.839195,9.26982 11.695975,22.37329 61.122555,37.89406 120.675335,37.89406 26.88168,0 50.69656,-2.86287 71.70015,-8.61931 20.63145,-5.65445 35.018,-12.926 43.62497,-22.04984 5.54328,-5.87617 7.13848,-9.2955 7.46577,-16.00293 0.23412,-4.79789 0.0844,-5.65403 -1.62989,-9.31727 -4.32313,-9.23833 -15.5707,-17.84079 -32.21932,-24.64228 -1.44697,-0.59114 -1.03028,-0.90017 5.27548,-3.91236 11.18139,-5.34124 10.44455,-5.22888 15.60069,-2.37895 16.8597,9.31877 28.89953,20.50336 34.55409,32.09957 3.94452,8.08932 4.19327,10.17957 4.20386,35.3248 0.005,12.39251 -0.27534,24.03372 -0.62347,25.86937 -0.79366,4.18498 -3.08408,9.89929 -5.8772,14.66288 -2.98248,5.08655 -15.09172,17.03919 -21.73492,21.45383 -21.39502,14.21776 -50.55421,23.93955 -84.30765,28.10852 -10.05863,1.24238 -32.44035,2.6586 -38.24337,2.4199 -1.58817,-0.0653 -5.05327,-0.19698 -7.70023,-0.29254 z m 43.95546,-8.25064 c 37.65585,-2.47121 61.66751,-10.37901 85.56877,-28.18049 7.74636,-5.76943 13.055,-10.67037 17.67557,-16.31808 l 3.5963,-4.39575 v -9.37864 -9.37863 l -1.6874,-1.10563 c -1.65737,-1.08595 -1.71567,-1.07194 -3.27637,0.78732 -8.4044,10.01213 -13.74326,14.75293 -25.51629,22.65798 -11.96612,8.03469 -25.11532,13.88852 -39.14282,17.42578 -11.91769,3.00524 -15.75826,3.5292 -45.07841,6.14999 l -4.01053,0.35848 v 9.17315 c 0,5.04524 0.18286,10.08745 0.40635,11.20492 0.39157,1.95785 0.50244,2.01989 3.04801,1.70518 1.45291,-0.17962 5.24048,-0.49713 8.41682,-0.70558 z m -81.17322,-49.2779 v -9.1892 l -10.15622,-1.10196 c -30.92164,-3.35501 -44.325232,-9.27441 -65.451715,-28.90529 -3.644712,-3.38669 -6.802444,-6.15762 -7.017184,-6.15762 -0.214739,0 -1.058752,0.62785 -1.875585,1.39523 -1.375077,1.29182 -1.485149,1.91022 -1.485149,8.34375 v 6.94852 l 4.65222,4.40034 c 19.627262,18.56461 31.989191,25.6234 51.859335,29.61225 6.223374,1.24932 23.520748,3.60415 27.709668,3.77233 l 1.76463,0.0709 z" + id="path4" + transform="scale(0.26458333)" /> + <path + style="fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:7.55905522;stroke-dasharray:none;stroke-opacity:1" + d="m 64.489394,236.55434 c -5.499491,-4.08191 -10.042641,-9.04853 -11.697252,-12.78755 -8.122381,-18.3546 20.546747,-37.55363 68.165578,-45.64885 17.05205,-2.89886 25.71854,-3.52988 48.7681,-3.55089 21.16213,-0.0193 26.30435,0.26981 41.54913,2.33598 l 5.29391,0.71749 v 19.15333 c 0,10.53433 -0.11259,19.15332 -0.25021,19.15332 -0.13761,0 -2.95301,-0.55407 -6.25643,-1.23128 -12.83673,-2.63155 -23.24358,-3.53219 -40.97809,-3.54636 -17.29632,-0.0138 -24.76577,0.55307 -38.35594,2.91102 -23.23495,4.03135 -45.343481,12.78557 -57.693143,22.8445 -1.866721,1.52047 -3.614687,2.76013 -3.884368,2.7548 -0.269682,-0.005 -2.36726,-1.4028 -4.661285,-3.10551 z" + id="path5" + transform="scale(0.26458333)" /> + <path + style="display:inline;fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:7.55906;stroke-dasharray:none;stroke-opacity:1" + d="m 266.89212,238.97365 c -1.39887,-1.54574 -8.85205,-6.84808 -11.00441,-7.82876 l -1.45941,-0.66495 v -21.66935 -21.66936 l 3.99655,1.62395 c 14.22776,5.78125 24.94953,13.89946 28.76411,21.77934 1.93642,4.0001 1.98113,9.14315 0.11543,13.27661 -1.75581,3.89 -8.09021,10.52599 -13.26736,13.89903 -4.53844,2.95691 -5.45743,3.11814 -7.14491,1.25349 z" + id="path6" + transform="scale(0.26458333)" /> + <path + style="display:inline;fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:7.55905522;stroke-dasharray:none;stroke-opacity:1" + d="m 245.12386,225.52342 c -4.98159,-2.41301 -21.84218,-8.31288 -23.75648,-8.31288 -0.81595,0 -0.94841,-2.72395 -0.94841,-19.5042 v -19.50421 l 8.50233,1.83338 c 9.39057,2.02491 11.74318,2.63014 17.64636,4.53973 l 4.01053,1.29735 v 21.1233 c 0,11.61782 -0.0722,21.09874 -0.16042,21.06871 -0.0882,-0.03 -2.47049,-1.17355 -5.29391,-2.54118 z" + id="path7" + transform="scale(0.26458333)" /> + <path + style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:7.55905522;stroke-dasharray:none;stroke-opacity:1" + d="m 239.02785,177.47169 -11.87118,-3.04292 9.87289,-4.9659 c 7.69578,-3.87085 10.24279,-4.8742 11.55034,-4.55003 2.78617,0.69074 19.96696,7.54589 19.94574,7.95836 -0.0278,0.5394 -15.82441,7.77641 -16.81453,7.70334 -0.44664,-0.033 -6.15411,-1.42925 -12.68326,-3.10285 z" + id="path8" + transform="scale(0.26458333)" /> + <path + style="fill:#4d4d4d;fill-opacity:1;stroke:#4d4d4d;stroke-width:7.55905522;stroke-dasharray:none;stroke-opacity:1" + d="m 280.41143,172.9527 c -0.74867,-0.57562 -2.99469,-1.7226 -4.99114,-2.54883 -1.99646,-0.82624 -6.69267,-2.7597 -10.43603,-4.29657 l -6.8061,-2.79433 4.76427,-1.98304 c 19.63336,-8.17204 34.70844,-16.14863 42.83477,-22.6649 0.97422,-0.78119 1.86764,-1.324 1.9854,-1.20624 0.46499,0.46498 -7.81743,23.99124 -9.04054,25.67969 -0.6921,0.95542 -2.45901,2.55247 -3.92646,3.54899 -2.38165,1.61735 -12.4137,7.32957 -12.85018,7.31686 -0.095,-0.003 -0.78532,-0.476 -1.53399,-1.05163 z" + id="path21" + transform="scale(0.26458333)" /> + <path + style="fill:#4d4d4d;fill-opacity:1;stroke:#4d4d4d;stroke-width:1.71492;stroke-dasharray:none;stroke-opacity:1" + d="m 305.13489,131.69809 c -0.0105,-1.99057 -2.7207,-12.73923 -4.69863,-18.63472 -2.86231,-8.53151 -5.86515,-15.342949 -14.00311,-31.763716 l -7.15607,-14.439522 v -3.406345 -3.406345 l 2.26961,2.483319 c 17.1607,18.776543 28.50323,35.015963 31.70968,45.399659 1.66024,5.3765 1.73014,8.62225 0.28081,13.03929 -1.51079,4.60434 -2.46019,6.25644 -5.3829,9.36715 -2.47412,2.63327 -3.01141,2.87549 -3.01939,1.36123 z" + id="path24" + transform="scale(0.26458333)" /> + <path + style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:3.42985;stroke-dasharray:none;stroke-opacity:1" + d="m 196.69637,318.62708 v -6.9488 l 9.86884,-0.7964 c 34.30687,-2.76851 54.4316,-8.87351 75.8788,-23.01848 8.47275,-5.58799 12.65037,-8.9161 18.9529,-15.0989 l 5.10458,-5.0076 v 5.44494 5.44493 l -2.57509,3.15103 c -9.31398,11.39713 -27.33358,24.29694 -43.70641,31.28841 -15.35717,6.55776 -33.93209,10.39531 -57.17126,11.8115 -2.37079,0.14448 -4.76995,0.35616 -5.33145,0.47042 l -1.02091,0.20773 z" + id="path27" + transform="scale(0.26458333)" /> + <path + style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:3.42985;stroke-dasharray:none;stroke-opacity:1" + d="m 110.48573,283.53277 c -29.783814,-3.57031 -41.373614,-8.79804 -62.956447,-28.3973 l -6.012057,-5.45952 v -4.0517 -4.0517 l 4.701083,4.24492 c 20.345292,18.3711 33.631315,24.29376 61.816271,27.55652 4.71715,0.54607 9.03606,1.08064 9.59756,1.18792 0.98138,0.1875 1.02092,0.37921 1.02092,4.95068 v 4.75563 l -1.47466,-0.055 c -0.81106,-0.0302 -3.82276,-0.33645 -6.69267,-0.68048 z" + id="path28" + transform="scale(0.26458333)" /> + </g> + <g + inkscape:groupmode="layer" + id="layer7" + inkscape:label="影" + style="fill:none;stroke:#333333"> + <g + id="g38" + style="fill:#1a1a1a;stroke:#1a1a1a"> + <path + style="fill:none;fill-opacity:1;stroke:#1a1a1a;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 5.4023439,62.787241 c 0.7203128,3.001303 1.4406252,6.002604 2.9012812,8.663782 1.460656,2.661177 3.6615659,4.982137 7.0630879,7.643322 3.401523,2.661186 8.003426,5.662428 12.56545,7.483224 4.562025,1.820797 9.083897,2.461062 13.605859,3.101339" + id="path29" + inkscape:path-effect="#path-effect29" + inkscape:original-d="m 5.4023439,62.787241 c 0.7203125,3.001303 1.440625,6.002604 2.1609375,9.003905 2.2009989,2.321055 4.4019096,4.642014 6.6028646,6.963021 4.602089,3.001365 9.203993,6.002605 13.80599,9.003909 4.522052,0.640289 9.043924,1.280554 13.565886,1.920832" + transform="matrix(1.0001242,0,0,0.98455811,-0.34244837,1.5416248)" /> + <path + style="fill:#1a1a1a;stroke:#1a1a1a;fill-opacity:1;stroke-width:3.77952761;stroke-dasharray:none;stroke-opacity:1" + d="m 106.62894,330.68298 c -0.49911,-0.11837 -4.17516,-1.10699 -8.168992,-2.19695 -38.526465,-10.51421 -65.797089,-28.87 -74.248535,-49.97648 -2.228394,-5.56515 -2.644076,-8.48872 -2.632724,-18.51643 l 0.01027,-9.0748 1.975958,5.89862 c 4.527219,13.51464 9.936565,22.2679 19.791868,32.0267 15.409494,15.25859 41.252101,32.2058 61.457195,40.30274 3.96392,1.58849 4.71495,2.22434 1.81496,1.5366 z" + id="path30" + transform="scale(0.26458333)" /> + </g> + <g + id="g37"> + <path + style="fill:none;fill-opacity:1;stroke:#1a1a1a;stroke-width:0.499999;stroke-dasharray:none;stroke-opacity:1" + d="m 27.01172,47.540626 c -2.841233,1.120486 -5.682467,2.240972 -6.672887,4.66206 -0.990421,2.421088 -0.130066,6.142627 0.910398,7.633267 1.040464,1.49064 2.26097,0.750334 3.481499,0.01001" + id="path35" + inkscape:path-effect="#path-effect35" + inkscape:original-d="m 27.01172,47.540626 c -2.841233,1.120487 -5.682466,2.240974 -8.523699,3.361458 0.860391,3.72169 1.720747,7.443229 2.58112,11.164845 1.220554,-0.740336 2.441059,-1.480643 3.661589,-2.220963" /> + <path + style="fill:#1a1a1a;stroke:#1a1a1a;fill-opacity:1;stroke-width:3.77952761;stroke-dasharray:none;stroke-opacity:1" + d="m 68.473549,239.59404 c -2.085761,-1.19268 -5.794176,-3.86317 -8.031421,-5.78355 -8.907229,-7.64569 -11.472756,-15.878 -7.465573,-23.95566 3.121498,-6.2923 10.357511,-12.46143 20.869658,-17.79261 3.073632,-1.55878 8.227689,-3.87348 8.624927,-3.87348 0.1168,0 -0.657204,0.83482 -1.720009,1.85515 -4.110405,3.94615 -6.008697,8.18856 -6.422372,14.35307 -0.687629,10.2469 3.243839,22.57049 8.006452,25.09705 0.571749,0.30331 1.039192,0.6027 1.038761,0.6653 -4.31e-4,0.0626 -1.148961,0.80819 -2.552288,1.65685 -3.254146,1.96794 -6.225676,4.08502 -9.058015,6.4534 -1.231574,1.02984 -2.262219,1.8712 -2.290324,1.86969 -0.0281,-0.001 -0.478012,-0.24685 -0.999796,-0.54521 z" + id="path36" + transform="scale(0.26458333)" /> + </g> + </g> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + style="display:inline"> + <path + id="ellipse2" + style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:1;stroke-dasharray:none" + d="m 44.999837,40.097294 a 39.902618,19.902618 0 0 0 -39.9024737,19.902661 39.902618,19.902618 0 0 0 0.013436,0.102836 h -0.00775 v 9.794235 h 0.00775 a 39.902618,19.902618 0 0 0 -0.013436,0.102836 39.902618,19.902618 0 0 0 39.9024737,19.902661 39.902618,19.902618 0 0 0 39.902991,-19.902661 39.902618,19.902618 0 0 0 -0.0057,-0.125057 v -9.772014 h -0.0078 a 39.902618,19.902618 0 0 0 0.01344,-0.102836 39.902618,19.902618 0 0 0 -39.902991,-19.902661 z" /> + </g> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Layer 2" + style="display:inline"> + <path + id="ellipse9" + style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:0.984034;stroke-dasharray:none;stroke-opacity:1" + d="M 71.080416,64.446195 A 29.481443,14.481445 0 0 0 44.616915,56.339197 29.481443,14.481445 0 0 0 18.462956,64.200216 32.00798,12.007983 0 0 0 44.999837,69.507902 32.00798,12.007983 0 0 0 71.080416,64.446195 Z" /> + <path + id="path10" + style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:0.984034;stroke-dasharray:none;stroke-opacity:1" + d="m 44.999837,45.491797 a 32.00798,12.007983 0 0 0 -32.007865,12.008052 32.00798,12.007983 0 0 0 5.470984,6.700367 29.481443,14.481445 0 0 1 26.153959,-7.861019 29.481443,14.481445 0 0 1 26.463501,8.106998 32.00798,12.007983 0 0 0 5.927803,-6.946346 32.00798,12.007983 0 0 0 -32.008382,-12.008052 z" /> + </g> + <g + inkscape:groupmode="layer" + id="layer3" + inkscape:label="Layer 3" + style="display:inline"> + <g + sodipodi:type="inkscape:box3d" + id="g10" + style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:3.77953;stroke-dasharray:none;stroke-opacity:1" + inkscape:perspectiveID="#perspective10" + inkscape:corner0="1.6697814 : 0.093104454 : 0 : 1" + inkscape:corner7="0.81871571 : 0.047558783 : 0.25 : 1"> + <path + sodipodi:type="inkscape:box3dside" + id="path16" + style="fill:#e9e9ff;fill-rule:evenodd;stroke:none;stroke-linejoin:round" + inkscape:box3dsidetype="11" + d="M 25.686855,23.820715 36.254378,13.050608 v 22.016399 l -10.567523,4.352709 z" + points="36.254378,13.050608 36.254378,35.067007 25.686855,39.419716 25.686855,23.820715 " /> + <path + sodipodi:type="inkscape:box3dside" + id="path11" + style="fill:#353564;fill-rule:evenodd;stroke:none;stroke-linejoin:round" + inkscape:box3dsidetype="6" + d="m 18.728125,21.369271 v 17.0597 l 6.95873,0.990745 V 23.820715 Z" + points="18.728125,38.428971 25.686855,39.419716 25.686855,23.820715 18.728125,21.369271 " /> + <path + sodipodi:type="inkscape:box3dside" + id="path12" + style="fill:#4d4d9f;fill-rule:evenodd;stroke:none;stroke-linejoin:round" + inkscape:box3dsidetype="5" + d="M 18.728125,21.369271 27.491927,7.9715577 36.254378,13.050608 25.686855,23.820715 Z" + points="27.491927,7.9715577 36.254378,13.050608 25.686855,23.820715 18.728125,21.369271 " /> + <path + sodipodi:type="inkscape:box3dside" + id="path15" + style="fill:#afafde;fill-rule:evenodd;stroke:none;stroke-linejoin:round" + inkscape:box3dsidetype="13" + d="m 18.728125,38.428971 8.763802,-5.414648 8.762451,2.052684 -10.567523,4.352709 z" + points="27.491927,33.014323 36.254378,35.067007 25.686855,39.419716 18.728125,38.428971 " /> + <path + sodipodi:type="inkscape:box3dside" + id="path14" + style="fill:#d7d7ff;fill-rule:evenodd;stroke:none;stroke-linejoin:round" + inkscape:box3dsidetype="14" + d="M 27.491927,7.9715577 V 33.014323 l 8.762451,2.052684 V 13.050608 Z" + points="27.491927,33.014323 36.254378,35.067007 36.254378,13.050608 27.491927,7.9715577 " /> + <path + sodipodi:type="inkscape:box3dside" + id="path13" + style="fill:#8686bf;fill-rule:evenodd;stroke:none;stroke-linejoin:round" + inkscape:box3dsidetype="3" + d="M 18.728125,21.369271 27.491927,7.9715577 V 33.014323 l -8.763802,5.414648 z" + points="27.491927,7.9715577 27.491927,33.014323 18.728125,38.428971 18.728125,21.369271 " /> + </g> + <path + style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 57.809846,46.434634 v 11.460101 l 8.998302,3.480477 V 48.471986 l -8.404074,-2.122241 7.13073,-3.565366 7.215618,2.886249 -5.772493,2.716469" + id="path22" /> + <g + id="g27" + style="fill:none;fill-opacity:1;stroke:#000000"> + <path + style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="M 67.109114,43.098698 C 74.872484,39.857291 82.635851,36.615885 83.676246,31.833748 84.71664,27.051611 79.034287,20.728993 73.351823,14.40625" + id="path23" /> + <path + style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.981698;stroke-dasharray:none;stroke-opacity:1" + d="M 83.830871,31.08324 80.145986,42.215902" + id="path25" /> + <path + style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.957444;stroke-dasharray:none;stroke-opacity:1" + d="m 73.437288,14.796161 -0.178212,3.539265" + id="path26" /> + </g> + <path + style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 76.653257,47.720704 c 1.340582,-1.260547 2.681162,-2.521093 3.341449,-3.501526 0.660287,-0.980433 0.640277,-1.680722 0.620267,-2.381026" + id="path19" + inkscape:path-effect="#path-effect19" + inkscape:original-d="m 76.653257,47.720704 c 1.340583,-1.260546 2.681163,-2.521092 4.021746,-3.781641 -0.02,-0.700318 -0.04002,-1.400607 -0.06003,-2.100911" + transform="rotate(16,79.627111,40.286277)" /> + <path + style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 80.434899,36.255729 c -0.400174,-2.24097 -0.800348,-4.481943 -1.960871,-7.463274 -1.160523,-2.981331 -3.081317,-6.702871 -5.00215,-10.424486" + id="path20" + inkscape:path-effect="#path-effect20" + inkscape:original-d="M 80.434899,36.255729 C 80.034724,34.014759 79.63455,31.773786 79.234376,29.532812 77.313506,25.811124 75.39271,22.089584 73.471878,18.367969" + transform="matrix(1.0226661,0,0,1.0264343,-1.8348178,-0.96020514)" /> + </g> + <g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="ハイライト" + style="display:inline"> + <g + id="g42" + style="stroke:#333333;stroke-opacity:1"> + <path + style="fill:none;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 10.314092,62.478776 c 2.971137,2.815507 5.942275,5.631015 9.627944,7.250996 3.685669,1.619982 8.085694,2.044422 12.485808,2.46887" + id="path38" + inkscape:path-effect="#path-effect38" + inkscape:original-d="m 10.314092,62.478776 c 2.971137,2.815508 5.942275,5.631014 8.913412,8.446522 4.400201,0.424455 8.800226,0.848894 13.20034,1.273344" /> + <path + style="fill:none;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 10.314092,62.478776 c 2.971137,2.815507 5.942275,5.631015 9.627944,7.250996 3.685669,1.619982 8.085694,2.044422 12.485808,2.46887" + id="path39" + inkscape:path-effect="#path-effect40" + inkscape:original-d="m 10.314092,62.478776 c 2.971137,2.815508 5.942275,5.631014 8.913412,8.446522 4.400201,0.424455 8.800226,0.848894 13.20034,1.273344" + transform="translate(0,3.7041669)" /> + <path + style="fill:none;fill-opacity:1;stroke:#333333;stroke-width:0.979179;stroke-dasharray:none;stroke-opacity:1" + d="M 10.4,62.859168 V 66.56248" + id="path40" /> + <path + style="fill:none;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 31.92,72.241086 v 3.650255" + id="path41" /> + </g> + <g + id="g18" + style="stroke:#333333"> + <path + style="fill:none;fill-opacity:1;stroke:#333333;stroke-width:1.01968;stroke-dasharray:none;stroke-opacity:1" + d="m 81.635416,69.330078 v 4.712044" + id="path45" /> + <path + style="display:inline;fill:none;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 51.022136,86.197397 c 3.601561,-0.240104 7.203125,-0.480209 10.584623,-1.120497 3.381498,-0.640289 6.542808,-1.68072 9.444093,-3.161381 2.901285,-1.480662 5.542376,-3.401456 7.28314,-4.922127 1.740764,-1.520672 2.581111,-2.641134 3.421476,-3.761621" + id="path42" + inkscape:path-effect="#path-effect42" + inkscape:original-d="m 51.022136,86.197397 c 3.601561,-0.240104 7.203125,-0.480209 10.804686,-0.720313 3.161435,-1.040474 6.322745,-2.080903 9.484116,-3.121355 2.641198,-1.920873 5.28229,-3.841666 7.923438,-5.762501 0.84038,-1.120508 1.680728,-2.24097 2.521092,-3.361457" + transform="translate(0,0.52916664)" /> + <path + style="display:inline;fill:none;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 51.022136,86.197397 c 3.601561,-0.240104 7.203125,-0.480209 10.584623,-1.120497 3.381498,-0.640289 6.542808,-1.68072 9.444093,-3.161381 2.901285,-1.480662 5.542376,-3.401456 7.28314,-4.922127 1.740764,-1.520672 2.581111,-2.641134 3.421476,-3.761621" + id="path43" + inkscape:path-effect="#path-effect44" + inkscape:original-d="m 51.022136,86.197397 c 3.601561,-0.240104 7.203125,-0.480209 10.804686,-0.720313 3.161435,-1.040474 6.322745,-2.080903 9.484116,-3.121355 2.641198,-1.920873 5.28229,-3.841666 7.923438,-5.762501 0.84038,-1.120508 1.680728,-2.24097 2.521092,-3.361457" + transform="translate(0,-4.2333336)" /> + <path + style="display:inline;fill:none;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 51.5,81.935546 v 4.682033" + id="path44" /> + </g> + <path + style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-dasharray:none;stroke-opacity:1" + d="m 50.169779,82.3854 v 3.735144" + id="path37" /> + </g> +</svg> diff --git a/src/components/organisms/infoTypeSelector/index.tsx b/src/components/organisms/infoTypeSelector/index.tsx index daee8353..c4f24bb9 100644 --- a/src/components/organisms/infoTypeSelector/index.tsx +++ b/src/components/organisms/infoTypeSelector/index.tsx @@ -33,7 +33,7 @@ const InfoTypeSelector: React.FunctionComponent<InfoTypeSelectorProps> = ({ { value: 'trap', alt: 'わなアイコン', - icon: '/static/images/icons/trap.svg', + icon: '/static/images/icons/trap-box.svg', label: 'わな情報', resolution: [32, 32], }, diff --git a/src/components/organisms/mapBase/base.tsx b/src/components/organisms/mapBase/base.tsx index ffa51cbe..1b9399f0 100644 --- a/src/components/organisms/mapBase/base.tsx +++ b/src/components/organisms/mapBase/base.tsx @@ -171,7 +171,11 @@ const MapBase_: React.FunctionComponent<MapBaseProps> = (props) => { }); }; const boarIconLink = '/static/images/icons/boar.svg'; - const trapIconLink = '/static/images/icons/trap.svg'; + const trapIconLink = { + 'box': '/static/images/icons/trap-box.svg', + 'tie': '/static/images/icons/trap-tie.svg', + 'gun': '/static/images/icons/trap-gun.svg', + }; const vaccineIconLink = '/static/images/icons/vaccine.svg'; const youtonIconLink = '/static/images/icons/youton.png'; const butanetsuIconLink = '/static/images/icons/butanetsu.png'; @@ -533,7 +537,25 @@ const MapBase_: React.FunctionComponent<MapBaseProps> = (props) => { } case 'わな設置地点': { const f2 = f as TrapFeature; - icon = markerIcon(trapIconLink, formatDate(f2.properties.設置年月日)); + let trapIconUrl = ''; + // わなの種類によってアイコンを変える + switch (f2.properties.罠の種類) { + case '箱わな': + trapIconUrl = trapIconLink.box; + break; + case 'くくりわな': + case '囲いわな': + // 暫定でくくりわなと囲いわなは同じアイコン + trapIconUrl = trapIconLink.tie; + break; + case '銃猟': + trapIconUrl = trapIconLink.gun; + break; + default: + // デフォルトは箱罠のアイコン(現在と同じ) + trapIconUrl = trapIconLink.box; + } + icon = markerIcon(trapIconUrl, formatDate(f2.properties.設置年月日)); dataLabel = '設置年月日'; dataValue = f2.properties.設置年月日; break; diff --git a/src/components/organisms/selectionMap/base.tsx b/src/components/organisms/selectionMap/base.tsx index 2a7ee85e..85a69c15 100644 --- a/src/components/organisms/selectionMap/base.tsx +++ b/src/components/organisms/selectionMap/base.tsx @@ -210,7 +210,11 @@ const SelectionMap_: React.FunctionComponent<SelectionMapProps> = (props) => { }); }; const boarIconLink = '/static/images/icons/boar.svg'; - const trapIconLink = '/static/images/icons/trap.svg'; + const trapIconLink = { + 'box': '/static/images/icons/trap-box.svg', + 'tie': '/static/images/icons/trap-tie.svg', + 'gun': '/static/images/icons/trap-gun.svg', + }; const vaccineIconLink = '/static/images/icons/vaccine.svg'; const youtonIconLink = '/static/images/icons/youton.png'; const butanetsuIconLink = '/static/images/icons/butanetsu.png'; @@ -574,7 +578,25 @@ const SelectionMap_: React.FunctionComponent<SelectionMapProps> = (props) => { } case 'わな設置地点': { const f2 = f as TrapFeature; - icon = markerIcon(trapIconLink, formatDate(f2.properties.設置年月日)); + let trapIconUrl = ''; + // わなの種類によってアイコンを変える + switch (f2.properties.罠の種類) { + case '箱わな': + trapIconUrl = trapIconLink.box; + break; + case 'くくりわな': + case '囲いわな': + // 暫定でくくりわなと囲いわなは同じアイコン + trapIconUrl = trapIconLink.tie; + break; + case '銃猟': + trapIconUrl = trapIconLink.gun; + break; + default: + // デフォルトは箱罠のアイコン(現在と同じ) + trapIconUrl = trapIconLink.box; + } + icon = markerIcon(trapIconUrl, formatDate(f2.properties.設置年月日)); dataLabel = '設置年月日'; dataValue = f2.properties.設置年月日; break; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c78febde..41b8433f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -62,7 +62,7 @@ export const layerLabels: { name: string; icon: string }[] = [ }, { name: 'わな設置地点', - icon: '/static/images/icons/trap.svg', + icon: '/static/images/icons/trap-box.svg', }, { name: 'ワクチン散布地点', From 262b81f99f101fb13fe18f7d6359ea82d7e46f96 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Thu, 16 Jan 2025 23:54:30 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20=E6=83=85=E5=A0=B1=E5=85=A5?= =?UTF-8?q?=E5=8A=9B=E6=99=82=E3=81=AB=E6=9B=B4=E6=96=B0=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=82=82=E5=85=A5=E5=8A=9B=E5=86=85=E5=AE=B9=E3=81=8C=E5=BC=95?= =?UTF-8?q?=E3=81=8D=E7=B6=99=E3=81=8C=E3=82=8C=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/atomos/miniMap/base.tsx | 1 + src/components/organisms/boarTable/index.tsx | 8 +- .../organisms/featureEditor/index.tsx | 2 - .../organisms/reportInfoForm/index.tsx | 47 +-- .../organisms/reportInfoForm/interface.ts | 3 - .../organisms/reportTable/index.tsx | 8 +- src/components/organisms/trapTable/index.tsx | 8 +- .../organisms/vaccineTable/index.tsx | 8 +- .../templates/addConfirmTemplate/index.tsx | 4 +- .../templates/addImageTemplate/index.tsx | 204 ------------- .../templates/addInfoTemplate/index.tsx | 136 --------- .../templates/addLocationTemplate/index.tsx | 170 ----------- .../templates/detailTemplate/index.tsx | 61 ++-- .../templates/editConfirmTemplate/index.tsx | 4 +- .../templates/editImageTemplate/index.tsx | 268 ------------------ .../templates/editInfoTemplate/index.tsx | 195 ------------- .../templates/editLocationTemplate/index.tsx | 130 --------- .../templates/mapTemplate/index.tsx | 3 + .../addTypeSelectorTemplate}/index.tsx | 50 ++-- .../newDataForm/commonInfoInput/index.tsx | 164 +++++++++++ .../newDataForm/dataConfirmTemplate/index.tsx | 235 +++++++++++++++ .../newDataForm/imageSelector/index.tsx | 228 +++++++++++++++ .../templates/newDataForm/interfaces.ts | 3 + .../newDataForm/locationSelector/index.tsx | 184 ++++++++++++ src/pages/add.tsx | 4 +- src/pages/add/confirm.tsx | 4 +- src/pages/add/image.tsx | 4 +- src/pages/add/info.tsx | 4 +- src/pages/add/location.tsx | 4 +- src/pages/edit/confirm.tsx | 4 +- src/pages/edit/image.tsx | 4 +- src/pages/edit/info.tsx | 4 +- src/pages/edit/location.tsx | 4 +- src/types/features.ts | 6 + src/utils/form-data.ts | 93 ++++++ src/utils/image.ts | 12 + 36 files changed, 1056 insertions(+), 1215 deletions(-) delete mode 100644 src/components/templates/addImageTemplate/index.tsx delete mode 100644 src/components/templates/addInfoTemplate/index.tsx delete mode 100644 src/components/templates/addLocationTemplate/index.tsx delete mode 100644 src/components/templates/editImageTemplate/index.tsx delete mode 100644 src/components/templates/editInfoTemplate/index.tsx delete mode 100644 src/components/templates/editLocationTemplate/index.tsx rename src/components/templates/{addTemplate => newDataForm/addTypeSelectorTemplate}/index.tsx (50%) create mode 100644 src/components/templates/newDataForm/commonInfoInput/index.tsx create mode 100644 src/components/templates/newDataForm/dataConfirmTemplate/index.tsx create mode 100644 src/components/templates/newDataForm/imageSelector/index.tsx create mode 100644 src/components/templates/newDataForm/interfaces.ts create mode 100644 src/components/templates/newDataForm/locationSelector/index.tsx create mode 100644 src/utils/form-data.ts create mode 100644 src/utils/image.ts diff --git a/src/components/atomos/miniMap/base.tsx b/src/components/atomos/miniMap/base.tsx index 20842259..27543dd9 100644 --- a/src/components/atomos/miniMap/base.tsx +++ b/src/components/atomos/miniMap/base.tsx @@ -6,6 +6,7 @@ import EventListener from 'react-event-listener'; import { parseCookies } from 'nookies'; import { SERVER_URI } from '../../../utils/constants'; import { getAccessToken } from '../../../utils/currentUser'; +import '../../../utils/extwms'; const MiniMap_: React.FunctionComponent<MiniMapProps> = (props) => { const [selfNode, setSelfNode] = useState<HTMLDivElement | null>(null); diff --git a/src/components/organisms/boarTable/index.tsx b/src/components/organisms/boarTable/index.tsx index 46079f88..36efd98a 100644 --- a/src/components/organisms/boarTable/index.tsx +++ b/src/components/organisms/boarTable/index.tsx @@ -80,7 +80,7 @@ const BoarTable: React.FunctionComponent<BoarTableProps> = (p) => { if (yesNoCheck) { router.push( { - pathname: '/edit/location', + pathname: '/edit-old/location', query: { id: id, type: 'いのしし捕獲地点', @@ -89,12 +89,12 @@ const BoarTable: React.FunctionComponent<BoarTableProps> = (p) => { detail: JSON.stringify(feature), }, }, - '/edit/location', + '/edit-old/location', ); } else { router.push( { - pathname: '/edit/image', + pathname: '/edit-old/image', query: { id: id, type: 'いのしし捕獲地点', @@ -103,7 +103,7 @@ const BoarTable: React.FunctionComponent<BoarTableProps> = (p) => { detail: JSON.stringify(feature), }, }, - '/edit/image', + '/edit-old/image', ); } }; diff --git a/src/components/organisms/featureEditor/index.tsx b/src/components/organisms/featureEditor/index.tsx index 19cc3cad..c45c8a6f 100644 --- a/src/components/organisms/featureEditor/index.tsx +++ b/src/components/organisms/featureEditor/index.tsx @@ -101,8 +101,6 @@ const FeatureEditor = React.forwardRef<FeatureEditorHandler, FeatureEditorProps> infoDiv = ( <ReportInfoForm ref={formRef} - objectURLs={objectURLs} - imageIds={imageIds} location={location} featureInfo={featureInfo as ReportFeature} /> diff --git a/src/components/organisms/reportInfoForm/index.tsx b/src/components/organisms/reportInfoForm/index.tsx index 823df8fa..05c4c59a 100644 --- a/src/components/organisms/reportInfoForm/index.tsx +++ b/src/components/organisms/reportInfoForm/index.tsx @@ -5,7 +5,6 @@ import { SERVER_URI } from '../../../utils/constants'; import { getAccessToken } from '../../../utils/currentUser'; import { checkDateError } from '../../../utils/validateData'; import WorkTimeInput from '../../atomos/workTimeInput'; -import InfoDiv from '../../molecules/infoDiv'; import InfoInput from '../../molecules/infoInput'; import { FeatureEditorHandler } from '../featureEditor/interface'; import { ReportInfoFormProps } from './interface'; @@ -44,10 +43,13 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp 作業終了時: time_end, 作業報告: report, 備考: note, - 画像ID: - props.featureInfo?.properties.画像ID != null - ? props.featureInfo?.properties.画像ID - : '', + 画像ID: '', + 錯誤捕獲: 'TODO', + 止刺道具: 'TODO', + 捕獲補助: 'TODO', + 作業内容: 'TODO', + ワクチンNO: 'TODO', + 市町村字: 'TODO' }, geometry: { type: 'Point', @@ -276,15 +278,6 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp return ( <div className='w-full'> <form id='form-report' onSubmit={(e) => e.preventDefault()}> - <InfoDiv - title='画像' - type='images' - data={{ - objectURLs: props.objectURLs == null ? [] : props.objectURLs.map((p) => p.objectURL), - imageIDs: props.imageIds == null ? [] : props.imageIds, - confirmMode: true, - }} - /> <InfoInput title='地域(農林事務所単位)' id='area' @@ -312,6 +305,22 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp error={errors.person_name} onChange={() => updateError('person_name', undefined)} /> + <InfoInput + title='わなの場所' + subtitle='市町村・字' + id='city' + type='text' + error={errors.city} + defaultValue={featureValueOrUndefined('市町村字')} + /> + <InfoInput + title='' + subtitle='ワクチンメッシュ番号' + id='vaccine_no' + type='text' + error={errors.vaccine} + defaultValue={featureValueOrUndefined('ワクチンNO')} + /> <WorkTimeInput id='worktime' onChange={onChangeWorktime} @@ -320,9 +329,11 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp required={true} error={errors.worktime} /> + <>ここに作業内容Window</> <InfoInput - title='作業報告・状況報告' - rows={2} + title='作業結果・状況報告' + subtitle='イノシシの痕跡、餌の摂食状況、わな設置場所の検討内容、わなの箇所数・基数、見回り活動で気づいたこと、餌で工夫したこと、改善点などを入力してください。' + rows={3} type='textarea' id='report' required={true} @@ -330,9 +341,11 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp error={errors.report} onChange={() => reportChanged()} /> + <>錯誤捕獲</> + <>ここに作業日報Bについて</> <InfoInput title='備考' - rows={5} + rows={3} type='textarea' id='note' defaultValue={featureValueOrUndefined('備考')} diff --git a/src/components/organisms/reportInfoForm/interface.ts b/src/components/organisms/reportInfoForm/interface.ts index 7e624ec5..74491e6b 100644 --- a/src/components/organisms/reportInfoForm/interface.ts +++ b/src/components/organisms/reportInfoForm/interface.ts @@ -1,11 +1,8 @@ import { ReportFeature } from '../../../types/features'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; import { LatLngZoom } from '../mapBase/interface'; export interface ReportInfoFormProps { location: LatLngZoom; featureInfo?: ReportFeature; - imageIds?: string[]; - objectURLs?: ImagewithLocation[]; isEditMode?: boolean; } diff --git a/src/components/organisms/reportTable/index.tsx b/src/components/organisms/reportTable/index.tsx index c3d10f85..f0cfc619 100644 --- a/src/components/organisms/reportTable/index.tsx +++ b/src/components/organisms/reportTable/index.tsx @@ -67,7 +67,7 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { if (yesNoCheck) { router.push( { - pathname: '/edit/location', + pathname: '/edit-old/location', query: { id: id, type: '作業日報', @@ -76,12 +76,12 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { version: 1, }, }, - '/edit/location', + '/edit-old/location', ); } else { router.push( { - pathname: '/edit/image', + pathname: '/edit-old/image', query: { id: id, type: '作業日報', @@ -90,7 +90,7 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { version: 1, }, }, - '/edit/image', + '/edit-old/image', ); } }; diff --git a/src/components/organisms/trapTable/index.tsx b/src/components/organisms/trapTable/index.tsx index d12f97c4..c17f8f29 100644 --- a/src/components/organisms/trapTable/index.tsx +++ b/src/components/organisms/trapTable/index.tsx @@ -67,7 +67,7 @@ const TrapTable: React.FunctionComponent<TrapTableProps> = (p) => { if (yesNoCheck) { router.push( { - pathname: '/edit/location', + pathname: '/edit-old/location', query: { id: id, type: 'わな設置地点', @@ -76,12 +76,12 @@ const TrapTable: React.FunctionComponent<TrapTableProps> = (p) => { version: 1, }, }, - '/edit/location', + '/edit-old/location', ); } else { router.push( { - pathname: '/edit/image', + pathname: '/edit-old/image', query: { id: id, type: 'わな設置地点', @@ -90,7 +90,7 @@ const TrapTable: React.FunctionComponent<TrapTableProps> = (p) => { version: 1, }, }, - '/edit/image', + '/edit-old/image', ); } }; diff --git a/src/components/organisms/vaccineTable/index.tsx b/src/components/organisms/vaccineTable/index.tsx index ab7568df..8083e0e5 100644 --- a/src/components/organisms/vaccineTable/index.tsx +++ b/src/components/organisms/vaccineTable/index.tsx @@ -67,7 +67,7 @@ const VaccineTable: React.FunctionComponent<VaccineTableProps> = (p) => { if (yesNoCheck) { router.push( { - pathname: '/edit/location', + pathname: '/edit-old/location', query: { id: id, type: 'ワクチン散布地点', @@ -76,12 +76,12 @@ const VaccineTable: React.FunctionComponent<VaccineTableProps> = (p) => { version: 1, }, }, - '/edit/location', + '/edit-old/location', ); } else { router.push( { - pathname: '/edit/image', + pathname: '/edit-old/image', query: { id: id, type: 'ワクチン散布地点', @@ -90,7 +90,7 @@ const VaccineTable: React.FunctionComponent<VaccineTableProps> = (p) => { version: 1, }, }, - '/edit/image', + '/edit-old/image', ); } }; diff --git a/src/components/templates/addConfirmTemplate/index.tsx b/src/components/templates/addConfirmTemplate/index.tsx index 30a7a74d..3fe6dfb3 100644 --- a/src/components/templates/addConfirmTemplate/index.tsx +++ b/src/components/templates/addConfirmTemplate/index.tsx @@ -189,7 +189,7 @@ const AddConfirmTemplate: React.FunctionComponent = () => { const onClickPrev = () => { router.push( { - pathname: '/add/info', + pathname: '/add-old/info', query: { type: router.query.type, images: router.query.images, @@ -197,7 +197,7 @@ const AddConfirmTemplate: React.FunctionComponent = () => { feature: router.query.feature, }, }, - '/add/info', + '/add-old/info', ); }; diff --git a/src/components/templates/addImageTemplate/index.tsx b/src/components/templates/addImageTemplate/index.tsx deleted file mode 100644 index cd0f6f6c..00000000 --- a/src/components/templates/addImageTemplate/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { to_header_color } from '../../../utils/header'; -import { alert } from '../../../utils/modal'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import RoundButton from '../../atomos/roundButton'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import ImageInput from '../../atomos/imageInput'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; -import { LayerType } from '../../../utils/gis'; -import { Location } from '../../organisms/mapBase/interface'; - -const AddImageTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [type, setType] = useState<string | null>(null); - const maxImageCount = type === 'report' ? 1 : 10; - - const prev_teeth = - router.query.teethImage == null || router.query.teethImage == '' - ? null - : (JSON.parse(router.query.teethImage as string) as ImagewithLocation); - const prev_other = - router.query.otherImages == null || router.query.otherImages == '' - ? null - : (JSON.parse(router.query.otherImages as string) as ImagewithLocation[]); - const prev_loc = - router.query.location == null - ? null - : (JSON.parse(router.query.location as string) as Location); - - const [teethImage, setTeethImage] = useState<ImagewithLocation | null>(prev_teeth); - const [otherImages, setOtherImages] = useState<ImagewithLocation[] | null>(prev_other); - - useEffect(() => { - const type = router.query.type as string | undefined; - if (type == null) { - alert('情報の取得に失敗しました。\nもう一度やり直してください。'); - router.push('/add'); - return; - } - - // 養豚場情報/豚熱陽性確認情報の場合は画像の登録がないのでスキップする。 - if (type === 'youton' || type === 'butanetsu') { - router.push( - { - pathname: '/add/location', - query: { - type: type, - images: '{}', - }, - }, - '/add/location', - ); - return; - } - - setType(type); - }, []); - - const onClickPrev = () => { - // Routerに位置情報が渡されている場合は情報登録画面からの遷移だからそっちに戻す - if (prev_loc != null) { - // push to /add/info - const prevData = { - teethImage: prev_teeth, - otherImages: prev_other, - }; - router.push( - { - pathname: '/add/info', - query: { - type: type, - images: JSON.stringify(prevData), - location: JSON.stringify(prev_loc), - }, - }, - '/add/info', - ); - } else { - router.push( - { - pathname: '/add', - query: { - type: type, - }, - }, - '/add', - ); - } - }; - - const onClickNext = async () => { - // Routerに位置情報が渡されている場合は情報登録画面からの遷移だからそっちに進める - const nextData = { - teethImage: teethImage, - otherImages: otherImages, - }; - if (prev_loc != null) { - router.push( - { - pathname: '/add/info', - query: { - type: type, - images: JSON.stringify(nextData), - location: JSON.stringify(prev_loc), - }, - }, - '/add/info', - ); - } else { - router.push( - { - pathname: '/add/location', - query: { - type: type, - images: JSON.stringify(nextData), - }, - }, - '/add/location', - ); - } - }; - - const onChangeTeeth = (files: ImagewithLocation[]) => { - if (files.length === 0) { - setTeethImage(null); - } else { - setTeethImage(files[0]); - } - }; - - const onChangeOthers = (files: ImagewithLocation[]) => { - if (files.length === 0) { - setOtherImages(null); - } else { - setOtherImages(files); - } - }; - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}>画像登録</Header> - <div className='mx-auto w-full max-w-[400px] bg-background py-3'> - <div className='mx-[15px] mt-2 text-justify'>画像を登録してください。</div> - <div className='mx-[15px] mt-2 text-justify'> - ※ {type == 'boar' ? 'その他の' : ''}画像は{maxImageCount}枚まで登録できます。 - </div> - {type == 'boar' ? ( - <> - <div className='mx-[15px] mt-2 text-justify'> - ※ <span className='font-bold'>有害捕獲の場合: </span><br /> - ・ 検体採取個体の歯列写真 - </div> - <div className='mx-[15px] mb-2 text-justify'> - ※ <span className='font-bold'>調査捕獲の場合: </span><br /> - ・ 検体採取個体の歯列写真 <br /> - ・ 全捕獲個体のそれぞれ全体の写真 - </div> - </> - ) : <></>} - {type == 'boar' ? ( - <div className='box-border w-full px-[15px] py-2'> - <div className='text-justify text-lg font-bold text-text'>歯列の画像</div> - <ImageInput - max_count={1} - type={type} - single_file={true} - onChange={onChangeTeeth} - objectURLs={teethImage == null ? undefined : [teethImage]} - /> - </div> - ) : ( - <></> - )} - <div className='box-border w-full px-[15px] py-2'> - <div className='text-justify text-lg font-bold text-text'> - {type == 'boar' ? 'その他の' : ''}画像 - <ImageInput - max_count={maxImageCount} - type={type as LayerType} - single_file={maxImageCount === 1} - onChange={onChangeOthers} - objectURLs={otherImages == null ? undefined : otherImages} - /> - </div> - </div> - </div> - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='primary' onClick={onClickNext.bind(this)}> - 進む > - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default AddImageTemplate; diff --git a/src/components/templates/addInfoTemplate/index.tsx b/src/components/templates/addInfoTemplate/index.tsx deleted file mode 100644 index de45d503..00000000 --- a/src/components/templates/addInfoTemplate/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useRouter } from 'next/router'; -import React from 'react'; -import { useEffect, useState } from 'react'; -import { FeatureBase } from '../../../types/features'; -import { to_header_color, to_header_title } from '../../../utils/header'; -import { alert } from '../../../utils/modal'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; -import RoundButton from '../../atomos/roundButton'; -import FeatureEditor from '../../organisms/featureEditor'; -import { FeatureEditorHandler } from '../../organisms/featureEditor/interface'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import { LatLngZoom } from '../../organisms/mapBase/interface'; - -const AddInfoTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [type, setType] = useState<string | null>(null); - const [images, setImages] = useState<Record< - string, - ImagewithLocation | ImagewithLocation[] | null - > | null>(null); - const [location, setLocation] = useState<LatLngZoom | null>(null); - const [feature, setFeature] = useState<FeatureBase | null>(null); - const [editorRef, setEditorRef] = useState<React.RefObject<FeatureEditorHandler> | null>(null); - const [imageArray, setImageArray] = useState<ImagewithLocation[] | null>(null); - const [isLoading, setLoading] = useState(false); - - useEffect(() => { - if (router.query.images == null || router.query.type == null || router.query.location == null) { - alert('情報の取得に失敗しました。\nもう一度やり直してください。'); - router.push('/add'); - return; - } - setType(router.query.type as string); - setImages(JSON.parse(router.query.images as string)); - setLocation(JSON.parse(router.query.location as string)); - - if (router.query.feature != null) { - setFeature(JSON.parse(router.query.feature as string)); - } - - if (editorRef == null) { - setEditorRef(React.createRef()); - } - }, []); - - const onClickNext = async () => { - if (editorRef == null) { - // 本来は起きないはず - alert('内部エラーが発生しました。'); - return; - } - setLoading(true); - - if (!(await editorRef.current?.validateData())) { - alert('入力内容にエラーがあります。ご確認ください。'); - setLoading(false); - return; - } - - const featureInfo = await editorRef.current?.fetchData(); - setLoading(false); - router.push( - { - pathname: '/add/confirm', - query: { - type: router.query.type, - images: router.query.images, - location: router.query.location, - feature: JSON.stringify(featureInfo), - }, - }, - '/add/confirm', - ); - }; - - const onClickPrev = () => { - router.push( - { - pathname: '/add/location', - query: { - type: router.query.type, - images: router.query.images, - prev_location: router.query.location, - }, - }, - '/add/location', - ); - }; - - useEffect(() => { - if (imageArray == null && images != null) { - const baseArr = - images == null || images.otherImages == null - ? [] - : (images.otherImages as ImagewithLocation[]); - if (images.teethImage != null) { - baseArr.unshift(images.teethImage as ImagewithLocation); - } - setImageArray(baseArr); - } - }, [imageArray, images]); - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}> - {to_header_title(type == null ? '' : type)}登録 - </Header> - {location != null ? ( - <FeatureEditor - type={type} - location={location} - featureInfo={feature == null ? undefined : feature} - ref={editorRef} - objectURLs={imageArray == null ? undefined : imageArray} - /> - ) : ( - <></> - )} - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='primary' onClick={onClickNext.bind(this)} disabled={isLoading}> - {isLoading ? '読み込み中...' : '進む >'} - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default AddInfoTemplate; diff --git a/src/components/templates/addLocationTemplate/index.tsx b/src/components/templates/addLocationTemplate/index.tsx deleted file mode 100644 index df10a727..00000000 --- a/src/components/templates/addLocationTemplate/index.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useRouter } from 'next/router'; -import { parseCookies } from 'nookies'; -import { useEffect, useState } from 'react'; -import { to_header_color } from '../../../utils/header'; -import { alert } from '../../../utils/modal'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; -import RoundButton from '../../atomos/roundButton'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import { LatLngZoom, LatLngZoomCookie, Location } from '../../organisms/mapBase/interface'; -import SelectionMap from '../../organisms/selectionMap'; - -const AddLocationTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [type, setType] = useState<string | null>(null); - const [defaultLoc, setDefaultLoc] = useState<LatLngZoom | null>(null); - const [mapDiv, setMapDiv] = useState<JSX.Element | null>(null); - const [currentLoc, setCurrentLoc] = useState<LatLngZoom | null>(null); - - const hasLocation = (t: string, imageInfo: Record<string, unknown>) => { - return ( - t == 'boar' && - imageInfo.teethImage != null && - (imageInfo.teethImage as ImagewithLocation).location != null && - router.query.prev_location == null - ); - }; - - useEffect(() => { - if (router.query.images == null || router.query.type == null) { - alert('情報の取得に失敗しました。\nもう一度やり直してください。'); - router.push('/add'); - return; - } - - setType(router.query.type as string); - - if (defaultLoc != null) return; - - // prev_locationがある場合はそっちから引っ張る - if (router.query.prev_location != null) { - const prevLocation = JSON.parse(router.query.prev_location as string) as LatLngZoom; - setDefaultLoc(prevLocation); - } else { - const imageInfo = JSON.parse(router.query.images as string); - - // ズーム率はCookieから引っ張ってくる - let defaultZoom = 17; - const cookies = parseCookies(null); - const last_geo = cookies['last_geo']; - let last_geo_obj = null; - if (last_geo != null) { - last_geo_obj = JSON.parse(last_geo) as LatLngZoomCookie; - defaultZoom = last_geo_obj.zoom; - } - - if (hasLocation(router.query.type as string, imageInfo)) { - // 画像に位置情報が存在した - const loc = imageInfo.teethImage.location as Location; - setDefaultLoc({ - isDefault: false, - zoom: defaultZoom, - ...loc, - }); - } else { - // 歯列画像がない or 画像に位置情報がない - // → 最後に表示していたところを中心にする。ない場合はデフォルト位置 - if (last_geo_obj == null) { - setDefaultLoc({ - isDefault: true, - zoom: defaultZoom, - lat: 35.39135, - lng: 136.722418, - }); - } else { - setDefaultLoc({ - isDefault: false, - ...last_geo_obj, - }); - } - } - } - }, []); - - useEffect(() => { - if (defaultLoc == null) return; - setMapDiv( - <SelectionMap - location={defaultLoc} - onCenterChanged={(loc) => setCurrentLoc(loc)} - isLoaded={hasLocation( - router.query.type as string, - JSON.parse(router.query.images as string), - )} - />, - ); - }, [defaultLoc]); - - const onClickPrev = () => { - // 画像登録がない2種の場合は2つ前の画面に飛ばす - if (type === 'youton' || type === 'butanetsu') { - router.push( - { - pathname: '/add', - query: { - type: type, - }, - }, - '/add', - ); - } else { - const imgData = JSON.parse(router.query.images as string) as Record< - string, - ImagewithLocation | ImagewithLocation[] | null - >; - router.push( - { - pathname: '/add/image', - query: { - type: type, - teethImage: - imgData.teethImage == null - ? undefined - : JSON.stringify(imgData.teethImage as ImagewithLocation | null), - otherImages: - imgData.otherImages == null - ? undefined - : JSON.stringify(imgData.otherImages as ImagewithLocation[] | null), - }, - }, - '/add/image', - ); - } - }; - - const onClickNext = () => { - router.push( - { - pathname: '/add/info', - query: { - type: type, - images: router.query.images, - location: JSON.stringify(currentLoc), - }, - }, - '/add/info', - ); - }; - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}>位置情報登録</Header> - {mapDiv} - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='primary' onClick={onClickNext.bind(this)}> - 進む > - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default AddLocationTemplate; diff --git a/src/components/templates/detailTemplate/index.tsx b/src/components/templates/detailTemplate/index.tsx index 6c33f828..b3da705f 100644 --- a/src/components/templates/detailTemplate/index.tsx +++ b/src/components/templates/detailTemplate/index.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useCurrentUser } from '../../../hooks/useCurrentUser'; import { FeatureBase } from '../../../types/features'; import { SERVER_URI } from '../../../utils/constants'; @@ -12,9 +12,11 @@ import RoundButton from '../../atomos/roundButton'; import FeatureViewer from '../../organisms/featureViewer'; import Footer from '../../organisms/footer'; import Header from '../../organisms/header'; +import { useFormDataParser } from '../../../utils/form-data'; const DetailTemplate: React.FunctionComponent = () => { const router = useRouter(); + const paramParser = useFormDataParser(); const { currentUser } = useCurrentUser(); const [featureInfo, setFeatureInfo] = useState<FeatureBase | null>(null); const [featureType, setFeatureType] = useState<string | null>(null); @@ -152,40 +154,41 @@ const DetailTemplate: React.FunctionComponent = () => { setEditable(true); }; - const onClickEdit = async () => { + const onClickEdit = useCallback(async () => { if (featureInfo == null) return; + const layerType = ( + (featureType ?? '').startsWith('boar-') ? 'boar' : featureType + ) as LayerType; + + const isImageSkip = layerType === 'report' || layerType === 'butanetsu' || layerType === 'youton'; + const yesNoCheck = await yesNo('位置情報の編集を行いますか?\n\n(いのしし捕獲情報のみ)\n※ 検体到着予定日以降に修正する場合は、下記にご連絡ください。\nTel. 058-272-8096 \n(平日8:30~12:00、13:00~17:15)'); + paramParser.updateData({ + dataType: layerType, + isLocationSkipped: !yesNoCheck, + isImageSkipped: isImageSkip, + inputData: { + gisData: featureInfo, + }, + editData: { + id: router.query.id as string, + type: router.query.type as string, + type_srv: featureType, + version: router.query.version as string, + curImg: { + teeth: ((featureInfo.properties as Record<string, string>)['歯列写真ID'] || '').split(','), + other: ((featureInfo.properties as Record<string, string>)[router.query.type_srv === 'boar-2' ? '写真ID' : '画像ID'] || '').split(','), + } + } + }); + if (yesNoCheck) { - router.push( - { - pathname: '/edit/location', - query: { - id: router.query.id as string, - type: router.query.type, - type_srv: featureType, - version: router.query.version, - detail: JSON.stringify(featureInfo), - }, - }, - '/edit/location', - ); + router.push('/edit/location'); } else { - router.push( - { - pathname: '/edit/image', - query: { - id: router.query.id as string, - type: router.query.type, - type_srv: featureType, - version: router.query.version, - detail: JSON.stringify(featureInfo), - }, - }, - '/edit/image', - ); + router.push('/edit/image'); } - }; + }, [featureInfo, featureType]); return ( <div className=''> diff --git a/src/components/templates/editConfirmTemplate/index.tsx b/src/components/templates/editConfirmTemplate/index.tsx index 1fd3d6f5..3ee2f59d 100644 --- a/src/components/templates/editConfirmTemplate/index.tsx +++ b/src/components/templates/editConfirmTemplate/index.tsx @@ -169,7 +169,7 @@ const EditConfirmTemplate: React.FunctionComponent = () => { const onClickPrev = () => { router.push( { - pathname: '/edit/info', + pathname: '/edit-old/info', query: { type: router.query.type, type_srv: router.query.type_srv, @@ -181,7 +181,7 @@ const EditConfirmTemplate: React.FunctionComponent = () => { serverImages: router.query.serverImages, }, }, - '/edit/info', + '/edit-old/info', ); }; diff --git a/src/components/templates/editImageTemplate/index.tsx b/src/components/templates/editImageTemplate/index.tsx deleted file mode 100644 index 3478c6c6..00000000 --- a/src/components/templates/editImageTemplate/index.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { BoarFeatureV2, FeatureBase } from '../../../types/features'; -import { LayerType } from '../../../utils/gis'; -import { to_header_color } from '../../../utils/header'; -import { alert } from '../../../utils/modal'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import ImageInput from '../../atomos/imageInput'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; -import RoundButton from '../../atomos/roundButton'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; - -const EditImageTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [type, setType] = useState<string | null>(null); - const maxImageCount = type === 'report' ? 1 : 10; - - const prev_teeth = - router.query.teethImage == null || router.query.teethImage == '' - ? null - : (JSON.parse(router.query.teethImage as string) as ImagewithLocation); - const prev_other = - router.query.otherImages == null || router.query.otherImages == '' - ? null - : (JSON.parse(router.query.otherImages as string) as ImagewithLocation[]); - const prev_srv = - router.query.serverImages == null || router.query.serverImages == '' - ? null - : (JSON.parse(router.query.serverImages as string) as Record<string, boolean | string[]>); - - const [teethImage, setTeethImage] = useState<ImagewithLocation | null>(prev_teeth); - const [otherImages, setOtherImages] = useState<ImagewithLocation[] | null>(prev_other); - const [serverImages] = useState<Record<string, boolean | string[]> | null>(prev_srv); - - const [isDeleteTeethImage, setIsDeleteTeethImage] = useState( - prev_srv != null ? prev_srv.isDeleteTeethImage : false, - ); - const [newImageIds, setNewImageIds] = useState<string[]>([]); - - const [imageKey, setImageKey] = useState<string | null>(null); - const [feature, setFeature] = useState<FeatureBase | null>(null); - - useEffect(() => { - if ( - router.query.type == null || - router.query.detail == null || - router.query.id == null || - router.query.version == null - ) { - alert('情報の取得に失敗しました。'); - router.push('/map'); - return; - } - - // 養豚場情報/豚熱陽性確認情報の場合は画像の登録がないのでスキップする。 - if (router.query.type === '養豚場' || router.query.type === '豚熱陽性高率エリア') { - let location = router.query.location; - if (location == null) { - const feature = JSON.parse(router.query.detail as string) as FeatureBase; - location = JSON.stringify({ - lat: feature.geometry.coordinates[1], - lng: feature.geometry.coordinates[0], - zoom: 17, - isDefault: false, - }); - } - - const images = JSON.stringify({ - teethImage: teethImage, - otherImages: otherImages, - }); - - const serverImages = JSON.stringify({ - isDeleteTeethImage: isDeleteTeethImage, - newServerImages: newImageIds, - }); - - router.push( - { - pathname: '/edit/info', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - detail: router.query.detail, - location: location, - images: images, - serverImages: serverImages, - }, - }, - '/edit/info', - ); - return; - } - - const key = router.query.type_srv === 'boar-2' ? '写真ID' : '画像ID'; - const f = JSON.parse(router.query.detail as string) as FeatureBase; - setType(router.query.type_srv as string); - setImageKey(key); - setFeature(f); - const lst = (f.properties as Record<string, string>)[key]; - setNewImageIds((lst == null ? '' : lst).split(',').filter((e) => e)); - }, []); - - const onClickNext = () => { - let location = router.query.location; - if (location == null) { - const feature = JSON.parse(router.query.detail as string) as FeatureBase; - location = JSON.stringify({ - lat: feature.geometry.coordinates[1], - lng: feature.geometry.coordinates[0], - zoom: 17, - isDefault: false, - }); - } - - const images = JSON.stringify({ - teethImage: teethImage, - otherImages: otherImages, - }); - - const serverImages = JSON.stringify({ - isDeleteTeethImage: isDeleteTeethImage, - newServerImages: newImageIds, - }); - - router.push( - { - pathname: '/edit/info', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - detail: router.query.detail, - location: location, - images: images, - serverImages: serverImages, - }, - }, - '/edit/info', - ); - }; - - const onClickPrev = () => { - if (router.query.location == null) { - // 位置情報がない場合はdetailから直接来た場合 - router.push( - { - pathname: '/detail', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - }, - }, - '/detail', - ); - } else { - // 位置情報がある場合は位置情報選択画面に戻す - router.push( - { - pathname: '/edit/location', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - detail: router.query.detail, - prev_location: router.query.location, - }, - }, - '/edit/location', - ); - } - }; - - const onChangeTeeth = (files: ImagewithLocation[]) => { - if (files.length === 0) { - setTeethImage(null); - } else { - setTeethImage(files[0]); - } - }; - - const onChangeOthers = (files: ImagewithLocation[]) => { - if (files.length === 0) { - setOtherImages(null); - } else { - setOtherImages(files); - } - }; - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}>登録画像編集</Header> - {imageKey != null && feature != null ? ( - <div className='mx-auto w-full max-w-[400px] bg-background py-3'> - <div className='mx-[15px] mt-2 text-justify'>画像を登録してください。</div> - <div className='mx-[15px] mb-2 text-justify'> - ※ {type == 'boar-2' ? 'その他の' : ''}画像は{maxImageCount}枚まで登録できます。 - </div> - {type == 'boar-2' ? ( - <div className='box-border w-full px-[15px] py-2'> - <div className='text-justify text-lg font-bold text-text'>歯列の画像</div> - <ImageInput - max_count={1} - type={type} - single_file={true} - onChange={onChangeTeeth} - objectURLs={teethImage == null ? undefined : [teethImage]} - imageIDs={ - serverImages != null && serverImages.isDeleteTeethImage - ? undefined - : [(feature as BoarFeatureV2).properties.歯列写真ID].filter((e) => e) - } - onServerImageDeleted={() => setIsDeleteTeethImage(true)} - /> - </div> - ) : ( - <></> - )} - <div className='box-border w-full px-[15px] py-2'> - <div className='text-justify text-lg font-bold text-text'> - {type == 'boar' ? 'その他の' : ''}画像 - <ImageInput - max_count={maxImageCount} - type={type as LayerType} - single_file={maxImageCount === 1} - onChange={onChangeOthers} - objectURLs={otherImages == null ? undefined : otherImages} - imageIDs={ - serverImages == null - ? ((feature.properties as Record<string, string>)[imageKey] != null - ? (feature.properties as Record<string, string>)[imageKey] - : '' - ) - .split(',') - .filter((e) => e) - : (serverImages.newServerImages as string[]) - } - onServerImageDeleted={(list) => setNewImageIds(list)} - /> - </div> - </div> - </div> - ) : ( - <></> - )} - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='primary' onClick={onClickNext.bind(this)}> - 進む > - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default EditImageTemplate; diff --git a/src/components/templates/editInfoTemplate/index.tsx b/src/components/templates/editInfoTemplate/index.tsx deleted file mode 100644 index 033c596e..00000000 --- a/src/components/templates/editInfoTemplate/index.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useRouter } from 'next/router'; -import React from 'react'; -import { useEffect, useState } from 'react'; -import { alert } from '../../../utils/modal'; -import { BoarFeatureV2, FeatureBase } from '../../../types/features'; -import { to_header_color, to_header_title } from '../../../utils/header'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; -import RoundButton from '../../atomos/roundButton'; -import FeatureEditor from '../../organisms/featureEditor'; -import { FeatureEditorHandler } from '../../organisms/featureEditor/interface'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import { LatLngZoom } from '../../organisms/mapBase/interface'; - -const EditInfoTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [type, setType] = useState<string | null>(null); - const [images, setImages] = useState<Record< - string, - ImagewithLocation | ImagewithLocation[] | null - > | null>(null); - const [location, setLocation] = useState<LatLngZoom | null>(null); - const [feature, setFeature] = useState<FeatureBase | null>(null); - const [editorRef, setEditorRef] = useState<React.RefObject<FeatureEditorHandler> | null>(null); - const [imageArray, setImageArray] = useState<ImagewithLocation[] | null>(null); - const [isLoading, setLoading] = useState(false); - const [serverImages, setServerImages] = useState<string[] | null>(null); - - useEffect(() => { - if ( - router.query.type == null || - router.query.id == null || - router.query.type_srv == null || - router.query.version == null || - router.query.detail == null || - router.query.location == null || - router.query.images == null || - router.query.serverImages == null - ) { - alert('情報の取得に失敗しました。'); - router.push('/map'); - return; - } - - let t = router.query.type_srv as string; - if (t === 'boar-1') { - t = 'boar-old'; - } else if (t === 'boar-2') { - t = 'boar'; - } - setType(t); - setImages(JSON.parse(router.query.images as string)); - if (router.query.location == '') { - router.query.location = undefined; - } - if (router.query.location != null) { - setLocation(JSON.parse(router.query.location as string)); - } - - const feature = router.query.detail != null ? JSON.parse(router.query.detail as string) : null; - if (router.query.detail != null) { - setFeature(feature); - } - - if (editorRef == null) { - setEditorRef(React.createRef()); - } - - const srvImgs = JSON.parse(router.query.serverImages as string); - const images = srvImgs['newServerImages'] as string[]; - if (t === 'boar' && !(srvImgs.isDeleteTeethImage as boolean)) { - images.unshift((feature as BoarFeatureV2).properties.歯列写真ID); - } - - setServerImages(images.filter((e) => e)); - }, []); - - const onClickPrev = () => { - if (router.query.type === '養豚場' || router.query.type === '豚熱陽性高率エリア') { - // 画像入力がない場合は前の画面を飛ばして戻す - router.push( - { - pathname: '/detail', - query: { - id: router.query.id, - version: '', - type: router.query.type, - }, - }, - '/detail', - ); - } else { - const old_imgs = JSON.parse(router.query.images as string); - router.push( - { - pathname: '/edit/image', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - detail: router.query.detail, - location: router.query.location, - serverImages: router.query.serverImages, - teethImage: JSON.stringify(old_imgs['teethImage']), - otherImages: JSON.stringify(old_imgs['otherImages']), - }, - }, - '/edit/image', - ); - } - }; - - const onClickNext = async () => { - if (editorRef == null) { - // 本来は起きないはず - alert('内部エラーが発生しました。'); - return; - } - setLoading(true); - - if (!(await editorRef.current?.validateData())) { - alert('入力内容にエラーがあります。ご確認ください。'); - setLoading(false); - return; - } - - const featureInfo = await editorRef.current?.fetchData(); - setLoading(false); - - router.push( - { - pathname: '/edit/confirm', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - detail: JSON.stringify(featureInfo), - location: router.query.location, - serverImages: router.query.serverImages, - images: router.query.images, - }, - }, - '/edit/confirm', - ); - }; - - useEffect(() => { - if (imageArray == null && images != null) { - const baseArr = - images == null || images.otherImages == null - ? [] - : (images.otherImages as ImagewithLocation[]); - if (images.teethImage != null) { - baseArr.unshift(images.teethImage as ImagewithLocation); - } - setImageArray(baseArr); - } - }, [imageArray, images]); - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}> - {to_header_title(type == null ? '' : type)}編集 - </Header> - {location != null ? ( - <FeatureEditor - type={type} - location={location} - featureInfo={feature == null ? undefined : feature} - ref={editorRef} - objectURLs={imageArray == null ? undefined : imageArray} - imageIds={serverImages == null ? undefined : serverImages} - /> - ) : ( - <></> - )} - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='primary' onClick={onClickNext.bind(this)} disabled={isLoading}> - {isLoading ? '読み込み中...' : '進む >'} - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default EditInfoTemplate; diff --git a/src/components/templates/editLocationTemplate/index.tsx b/src/components/templates/editLocationTemplate/index.tsx deleted file mode 100644 index e2551497..00000000 --- a/src/components/templates/editLocationTemplate/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useRouter } from 'next/router'; -import { parseCookies } from 'nookies'; -import { useEffect, useState } from 'react'; -import { FeatureBase } from '../../../types/features'; -import { to_header_color } from '../../../utils/header'; -import { alert } from '../../../utils/modal'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import RoundButton from '../../atomos/roundButton'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import { LatLngZoom, LatLngZoomCookie } from '../../organisms/mapBase/interface'; -import SelectionMap from '../../organisms/selectionMap'; - -const EditLocationTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [type, setType] = useState<string | null>(null); - const [defaultLoc, setDefaultLoc] = useState<LatLngZoom | null>(null); - const [mapDiv, setMapDiv] = useState<JSX.Element | null>(null); - const [, setCurrentLoc] = useState<LatLngZoom | null>(null); - - useEffect(() => { - if ( - router.query.type == null || - router.query.detail == null || - router.query.id == null || - router.query.version == null - ) { - alert('情報の取得に失敗しました。'); - router.push('/map'); - return; - } - - setType(router.query.type as string); - - if (defaultLoc != null) return; - - // prev_locationがある場合はそっちから引っ張る - if (router.query.prev_location != null) { - const prevLocation = JSON.parse(router.query.prev_location as string) as LatLngZoom; - setDefaultLoc(prevLocation); - } else { - // ズーム率はCookieから引っ張ってくる - let defaultZoom = 17; - const cookies = parseCookies(null); - const last_geo = cookies['last_geo']; - let last_geo_obj = null; - if (last_geo != null) { - last_geo_obj = JSON.parse(last_geo) as LatLngZoomCookie; - defaultZoom = last_geo_obj.zoom; - } - - // フィーチャーから位置情報を引っ張ってくる。 - - const featureInfo = JSON.parse(router.query.detail as string) as FeatureBase; - const coordinates = featureInfo.geometry.coordinates; - setDefaultLoc({ - isDefault: false, - zoom: defaultZoom, - lat: coordinates[1], - lng: coordinates[0], - }); - } - }, []); - - useEffect(() => { - if (defaultLoc == null) return; - setMapDiv( - <SelectionMap - location={defaultLoc} - onCenterChanged={(loc) => setCurrentLoc(loc)} - isLoaded={false} - />, - ); - }, [defaultLoc]); - - const onClickNext = () => { - setCurrentLoc((loc) => { - router.push( - { - pathname: '/edit/image', - query: { - id: router.query.id as string, - type: router.query.type, - type_srv: router.query.type_srv, - version: router.query.version, - detail: router.query.detail, - location: JSON.stringify(loc), - }, - }, - '/edit/image', - ); - return loc; - }); - }; - - const onClickPrev = () => { - router.push( - { - pathname: '/detail', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - }, - }, - '/detail', - ); - }; - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}>位置情報編集</Header> - {mapDiv} - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='primary' onClick={onClickNext.bind(this)}> - 進む > - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default EditLocationTemplate; diff --git a/src/components/templates/mapTemplate/index.tsx b/src/components/templates/mapTemplate/index.tsx index 4c2b4460..2233e7b2 100644 --- a/src/components/templates/mapTemplate/index.tsx +++ b/src/components/templates/mapTemplate/index.tsx @@ -4,11 +4,14 @@ import RoundButton from '../../atomos/roundButton'; import Footer from '../../organisms/footer'; import Header from '../../organisms/header'; import MapBase from '../../organisms/mapBase'; +import { useFormDataParser } from '../../../utils/form-data'; const MapTemplate: React.FunctionComponent = () => { + const formParser = useFormDataParser(); const router = useRouter(); const onClickAdd = () => { + formParser.updateData(null); router.push('/add'); }; diff --git a/src/components/templates/addTemplate/index.tsx b/src/components/templates/newDataForm/addTypeSelectorTemplate/index.tsx similarity index 50% rename from src/components/templates/addTemplate/index.tsx rename to src/components/templates/newDataForm/addTypeSelectorTemplate/index.tsx index d8a5aa98..da8e817d 100644 --- a/src/components/templates/addTemplate/index.tsx +++ b/src/components/templates/newDataForm/addTypeSelectorTemplate/index.tsx @@ -1,19 +1,21 @@ -import { useRouter } from 'next/router'; -import { useState } from 'react'; -import { useCurrentUser } from '../../../hooks/useCurrentUser'; -import { hasWritePermission, LayerType } from '../../../utils/gis'; -import { alert } from '../../../utils/modal'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import RoundButton from '../../atomos/roundButton'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import InfoTypeSelector from '../../organisms/infoTypeSelector'; +import { useState } from "react"; +import { useCurrentUser } from "../../../../hooks/useCurrentUser"; +import { hasWritePermission, LayerType } from "../../../../utils/gis"; +import { useRouter } from "next/router"; +import { useFormDataParser } from "../../../../utils/form-data"; +import Header from "../../../organisms/header"; +import InfoTypeSelector from "../../../organisms/infoTypeSelector"; +import FooterAdjustment from "../../../atomos/footerAdjustment"; +import Footer from "../../../organisms/footer"; +import RoundButton from "../../../atomos/roundButton"; + +const AddTypeSelectorTemplate = () => { + const paramParser = useFormDataParser(); -const AddTemplate: React.FunctionComponent = () => { const router = useRouter(); const { currentUser } = useCurrentUser(); const [selected, setSelected] = useState<LayerType | null>( - router.query.type == null ? null : (router.query.type as LayerType), + paramParser.currentData.dataType ?? null ); const onClickNext = async () => { @@ -27,15 +29,17 @@ const AddTemplate: React.FunctionComponent = () => { return; } - router.push( - { - pathname: '/add/image', - query: { - type: selected, - }, - }, - '/add/image', - ); + const isImageSkip = selected === 'report' || selected === 'butanetsu' || selected === 'youton'; + + paramParser.updateData({ dataType: selected, isLocationSkipped: false, isImageSkipped: isImageSkip, inputData: {} }); + + // 作業日報と豚熱要請確認情報、養豚場情報は画像の登録が必要ないので直接位置情報ページへ遷移 + // それ以外は画像の登録ページへ遷移 + if (isImageSkip) { + router.push('/add/location'); + } else { + router.push('/add/image'); + } }; return ( @@ -45,7 +49,7 @@ const AddTemplate: React.FunctionComponent = () => { <div className='mx-4 mt-1 mb-1'>情報の種類を選択してください。</div> <InfoTypeSelector onChanged={(type) => setSelected(type)} - defaultValue={router.query.type == null ? null : (router.query.type as LayerType)} + defaultValue={paramParser.currentData.dataType ?? null} /> </div> <FooterAdjustment /> @@ -67,4 +71,4 @@ const AddTemplate: React.FunctionComponent = () => { ); }; -export default AddTemplate; +export default AddTypeSelectorTemplate; \ No newline at end of file diff --git a/src/components/templates/newDataForm/commonInfoInput/index.tsx b/src/components/templates/newDataForm/commonInfoInput/index.tsx new file mode 100644 index 00000000..cf334fd7 --- /dev/null +++ b/src/components/templates/newDataForm/commonInfoInput/index.tsx @@ -0,0 +1,164 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { to_header_color, to_header_title } from "../../../../utils/header"; +import FooterAdjustment from "../../../atomos/footerAdjustment"; +import RoundButton from "../../../atomos/roundButton"; +import Footer from "../../../organisms/footer"; +import Header from "../../../organisms/header"; +import { InputFormTemplateCommonProps } from "../interfaces"; +import { useRouter } from "next/router"; +import { InputFormData, useFormDataParser } from "../../../../utils/form-data"; +import FeatureEditor from "../../../organisms/featureEditor"; +import { FeatureEditorHandler } from "../../../organisms/featureEditor/interface"; +import React from "react"; +import { ImagewithLocation } from "../../../atomos/imageInput/interface"; +import { alert } from '../../../../utils/modal'; + +const CommonInfoInputTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEditing }) => { + const router = useRouter(); + const paramParser = useFormDataParser(); + + const [editorRef, setEditorRef] = useState<React.RefObject<FeatureEditorHandler> | null>(null); + const [imageArray, setImageArray] = useState<ImagewithLocation[] | null>(null); + const [serverImages, setServerImages] = useState<string[] | null>(null); + + const type = paramParser.currentData.dataType; + const t = useMemo(() => { + const tt = paramParser.currentData.editData?.type_srv; + if (tt == null) { + return paramParser.currentData.dataType; + } else if(tt === 'boar-1') { + return 'boar-old'; + } else if(tt === 'boar-2') { + return 'boar'; + } + + return tt; + }, [paramParser.currentData]); + + const location = useMemo(() => { + return { + isDefault: false, + lat: paramParser.currentData.inputData.gisData?.geometry?.coordinates[1] ?? 0, + lng: paramParser.currentData.inputData.gisData?.geometry?.coordinates[0] ?? 0, + zoom: 17 + }; + }, [paramParser.currentData]); + + const [isValidating, setIsValidating] = useState(false); + + useEffect(() => { + if (!paramParser.currentData.dataType) { + alert('情報の取得に失敗しました。'); + router.push('/map'); + return; + } + + if (editorRef == null) { + setEditorRef(React.createRef()); + } + + setImageArray((paramParser.currentData.inputData.teethImageUrls ?? []).concat(paramParser.currentData.inputData.otherImageUrls ?? [])); + setServerImages(() => { + const featureProps = paramParser.currentData.inputData?.gisData?.properties as Record<string, string>; + if (!featureProps) + return []; + + const arr + = (featureProps['歯列写真ID'] || '') + .split(',') + .concat((featureProps['写真ID'] || '').split(',')) + .concat((featureProps['画像ID'] || '').split(',')) + .filter((e) => e); + return arr; + }); + }, [paramParser.currentData, editorRef]); + + const onClickPrev = useCallback(() => { + if (isEditing) { + if (paramParser.currentData.isImageSkipped) { + if (paramParser.currentData.isLocationSkipped) { + router.push( + { + pathname: '/detail', + query: { + type: paramParser.currentData.editData?.type, + type_srv: paramParser.currentData.editData?.type_srv, + id: paramParser.currentData.editData?.id, + version: paramParser.currentData.editData?.version + } + }, + '/detail' + ); + } else { + router.push('/edit/location'); + } + } else { + router.push('/edit/image'); + } + } else { + router.push('/add/location'); + } + }, []); + + const onClickNext = useCallback(async () => { + if (editorRef == null) { + // 本来は起きないはず + alert('内部エラーが発生しました。'); + return; + } + setIsValidating(true); + + if (!(await editorRef.current?.validateData())) { + alert('入力内容にエラーがあります。ご確認ください。'); + setIsValidating(false); + return; + } + + const featureInfo = await editorRef.current?.fetchData(); + if (featureInfo == null) { + alert('情報の取得に失敗しました。'); + setIsValidating(false); + return; + } + setIsValidating(false); + + const newData = JSON.parse(JSON.stringify(paramParser.currentData)) as InputFormData; + newData.inputData.gisData = featureInfo; + paramParser.updateData(newData); + + if (isEditing) { + router.push('/edit/confirm'); + } else { + router.push('/add/confirm'); + } + }, [editorRef, paramParser.currentData]); + + return ( + <div> + <Header color={to_header_color(type == null ? '' : type)}> + {to_header_title(type == null ? '' : type)}{isEditing ? '編集' : '登録'} + </Header> + <FeatureEditor + type={t} + location={location} + featureInfo={paramParser.currentData.inputData.gisData} + ref={editorRef} + objectURLs={imageArray == null ? undefined : imageArray} + imageIds={serverImages == null ? undefined : serverImages} + /> + <FooterAdjustment /> + <div className='fixed bottom-0 w-full'> + <Footer> + <RoundButton color='accent' onClick={onClickPrev.bind(this)}> + < 戻る + </RoundButton> + <RoundButton color='primary' onClick={onClickNext.bind(this)} disabled={isValidating}> + {isValidating ? '読み込み中...' : '進む >'} + </RoundButton> + </Footer> + </div> + </div> + ); +}; + +export default CommonInfoInputTemplate; \ No newline at end of file diff --git a/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx b/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx new file mode 100644 index 00000000..dd14054a --- /dev/null +++ b/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx @@ -0,0 +1,235 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { to_header_color, to_header_title } from "../../../../utils/header"; +import FooterAdjustment from "../../../atomos/footerAdjustment"; +import RoundButton from "../../../atomos/roundButton"; +import FeatureViewer from "../../../organisms/featureViewer"; +import Footer from "../../../organisms/footer"; +import Header from "../../../organisms/header"; +import { InputFormTemplateCommonProps } from "../interfaces"; +import { InputFormData, useFormDataParser } from "../../../../utils/form-data"; +import { useRouter } from "next/router"; +import { BoarFeatureV2, FeatureBase } from "../../../../types/features"; +import { alert, confirm } from "../../../../utils/modal"; +import { getAccessToken } from "../../../../utils/currentUser"; +import { SERVER_URI } from "../../../../utils/constants"; + +const DataConfirmTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEditing }) => { + const router = useRouter(); + const paramParser = useFormDataParser(); + + const [imageArray, setImageArray] = useState<string[] | null>(null); + const [serverImages, setServerImages] = useState<string[] | null>(null); + const [featureInfo, setFeatureInfo] = useState<FeatureBase | null>(null); + + const type = paramParser.currentData.dataType; + const t = useMemo(() => { + const tt = paramParser.currentData.editData?.type_srv; + if (tt == null) { + return paramParser.currentData.dataType; + } else if(tt === 'boar-1') { + return 'boar-old'; + } else if(tt === 'boar-2') { + return 'boar'; + } + + return tt; + }, [paramParser.currentData]); + + const [isLoading, setIsLoading] = useState(false); + + const deleteImage = (id: string) => { + return new Promise<void>((resolve, reject) => { + try { + const data = new FormData(); + data.append('id', id); + const options = { + method: 'POST', + body: data, + headers: { + Accept: 'application/json', + 'X-Access-Token': getAccessToken(), + }, + }; + fetch(`${SERVER_URI}/Image/DeleteImage`, options) + .then((res) => { + if (res.status === 200 || res.status === 404) { + resolve(); + } else { + return res.json(); + } + }) + .then((json) => reject(json['reason'])) + .catch((e) => reject(e)); + } catch (e) { + reject(e); + } + }); + }; + + const uploadImage = useCallback(async (objectURL: string) => { + const url = `${SERVER_URI}/Image/AddImage?type=${paramParser.currentData.dataType}`; + + const body = new FormData(); + const blob = await fetch(objectURL).then((r) => r.blob()); + body.append('files[]', blob); + + const req = { + method: 'POST', + body: body, + headers: { + 'X-Access-Token': getAccessToken(), + }, + }; + + const r = await fetch(url, req); + const json = await r.json(); + if (json['status'] == 200) { + const resList = json['results'] as Record<string, unknown>[]; + if (resList.length > 0) { + return resList[0]['id'] as string; + } + } else { + console.error(json['reason']); + } + + return null; + }, [paramParser.currentData]); + + const onClickPrev = useCallback(() => { + if (isEditing) { + router.push('/edit/info'); + } else { + router.push('/add/info'); + } + }, []); + + const onClickNext = useCallback(async () => { + if (await confirm('この内容でよろしいですか?')) { + const newData = JSON.parse(JSON.stringify(paramParser.currentData)) as InputFormData; + + if (newData.inputData.gisData == undefined) { + alert("エラーが発生しました。"); + return; + } + + // 削除される画像リストの取得 + const origImg = paramParser.currentData.editData?.curImg.other.concat(paramParser.currentData.editData?.curImg.teeth) || []; + const delImg = origImg.filter(e => !serverImages?.includes(e)); + + setIsLoading(true); + + // 画像を削除 + await Promise.all(delImg.map(e => deleteImage(e))); + + // 対象の場合は歯列写真をアップロードしてFeatureにセット + if((!isEditing && paramParser.currentData.dataType === 'boar') || (isEditing && paramParser.currentData.editData?.type_srv === 'boar-2')) { + const teethImageIds = (await Promise.all((paramParser.currentData.inputData.teethImageUrls || []).map(e => uploadImage(e.objectURL)))).filter(e => e != null); + + const currentIds = (newData.inputData.gisData.properties as Record<string, string>)["歯列写真ID"].split(","); + const newIds = currentIds.concat(teethImageIds).filter(e => e); + (newData.inputData.gisData.properties as Record<string, string>)['歯列写真ID'] = newIds.join(','); + } + + // 画像をアップロードしてFeatureにセット + const otherImageIds = (await Promise.all((paramParser.currentData.inputData.otherImageUrls || []).map(e => uploadImage(e.objectURL)))).filter(e => e != null); + const currentIds = (newData.inputData.gisData.properties as Record<string, string>)[t == "boar" ? "写真ID" : "画像ID"].split(","); + const newIds = currentIds.concat(otherImageIds).filter(e => e); + + (newData.inputData.gisData.properties as Record<string, string>)[t == "boar" ? "写真ID" : "画像ID"] = newIds.join(','); + + let res = null; + if (isEditing) { + res = await fetch(SERVER_URI + '/Features/UpdateFeature', { + method: 'POST', + headers: { + 'X-Access-Token': getAccessToken(), + }, + body: JSON.stringify({ + type: newData.editData?.type_srv, + feature: newData.inputData.gisData, + }), + }); + } else { + res = await fetch(SERVER_URI + '/Features/AddFeature', { + method: 'POST', + headers: { + 'X-Access-Token': getAccessToken(), + }, + body: JSON.stringify({ + type: newData.dataType, + feature: newData.inputData.gisData, + }), + }); + } + + const json = await res.json(); + + if (res.status === 200) { + await alert('登録が完了しました。\nご協力ありがとうございました。'); + setIsLoading(false); + router.push('/map'); + } else { + console.error(json['error']); + await alert('エラーが発生しました。\n' + json['error']); + } + + setIsLoading(false); + } + }, [serverImages, paramParser.currentData, t]); + + useEffect(() => { + if (!paramParser.currentData.dataType) { + alert('情報の取得に失敗しました。'); + router.push('/map'); + return; + } + + setImageArray((paramParser.currentData.inputData.teethImageUrls ?? []).concat(paramParser.currentData.inputData.otherImageUrls ?? []).map(e => e.objectURL)); + setServerImages(() => { + const featureProps = paramParser.currentData.inputData?.gisData?.properties as Record<string, string>; + if (!featureProps) + return []; + + const arr + = (featureProps['歯列写真ID'] || '') + .split(',') + .concat((featureProps['写真ID'] || '').split(',')) + .concat((featureProps['画像ID'] || '').split(',')) + .filter((e) => e); + return arr; + }); + + setFeatureInfo(paramParser.currentData.inputData.gisData as FeatureBase); + }, []); + + return ( + <div> + <Header color={to_header_color(type == null ? '' : type)}> + {to_header_title(type == null ? '' : type)}{isEditing ? '編集' : '登録'} + </Header> + <div className='mx-auto w-full max-w-[400px] bg-background py-3'> + <div className='mx-[15px] mt-2 text-justify'>情報に不備がないかご確認ください。</div> + </div> + <FeatureViewer + featureInfo={featureInfo} + type={t} + objectURLs={imageArray == null ? undefined : imageArray} + imageIDs={serverImages == null ? undefined : serverImages} + confirm={true} + /> + <FooterAdjustment /> + <div className='fixed bottom-0 w-full'> + <Footer> + <RoundButton color='accent' onClick={onClickPrev.bind(this)}> + < 戻る + </RoundButton> + <RoundButton color='danger' onClick={onClickNext.bind(this)} disabled={isLoading}> + {isLoading ? '読み込み中...' : '登録 >'} + </RoundButton> + </Footer> + </div> + </div> + ); +}; + +export default DataConfirmTemplate; \ No newline at end of file diff --git a/src/components/templates/newDataForm/imageSelector/index.tsx b/src/components/templates/newDataForm/imageSelector/index.tsx new file mode 100644 index 00000000..afda25ae --- /dev/null +++ b/src/components/templates/newDataForm/imageSelector/index.tsx @@ -0,0 +1,228 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { InputFormData, useFormDataParser } from "../../../../utils/form-data"; +import { to_header_color } from "../../../../utils/header"; +import FooterAdjustment from "../../../atomos/footerAdjustment"; +import RoundButton from "../../../atomos/roundButton"; +import Footer from "../../../organisms/footer"; +import Header from "../../../organisms/header"; +import { InputFormTemplateCommonProps } from "../interfaces"; +import { useRouter } from "next/router"; +import ImageInput from "../../../atomos/imageInput"; +import { ImagewithLocation } from "../../../atomos/imageInput/interface"; +import { alert } from '../../../../utils/modal'; + +const ImageSelectorTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEditing }) => { + const router = useRouter(); + const paramParser = useFormDataParser(); + + const type = paramParser.currentData.dataType; + + const currentServerOtherImageIds = useMemo(() => { + if (!isEditing) return []; + + const featureProps = paramParser.currentData.inputData?.gisData?.properties as Record<string, string> | undefined; + if (!featureProps) return []; + + const t = paramParser.currentData.editData?.type_srv; + + return (featureProps[t === 'boar-2' ? '写真ID' : '画像ID'] ?? '').split(',').filter((e) => e); + }, [isEditing, paramParser.currentData.inputData?.gisData, paramParser.currentData.editData?.type_srv]); + const [newOtherImageIds, setNewOtherImageIds] = useState<string[]>(currentServerOtherImageIds); + const [otherImages, setOtherImages] = useState<ImagewithLocation[] | null>(paramParser.currentData.inputData?.otherImageUrls ?? null); + + const currentServerTeethImageIds = useMemo(() => { + if (!isEditing) return []; + + const featureProps = paramParser.currentData.inputData?.gisData?.properties as Record<string, string> | undefined; + if (!featureProps) return []; + + const t = paramParser.currentData.editData?.type_srv; + if (t !== 'boar-2') + return []; + + return featureProps['歯列写真ID'].split(',').filter((e) => e); + }, [isEditing, paramParser.currentData.inputData?.gisData, paramParser.currentData.editData?.type_srv]); + const [newTeethImageIds, setNewTeethImageIds] = useState<string[]>(currentServerTeethImageIds); + const [teethImages, setTeethImages] = useState<ImagewithLocation[] | null>(paramParser.currentData.inputData?.teethImageUrls?? null); + + + useEffect(() => { + if (!paramParser.currentData.dataType) { + alert('情報の取得に失敗しました。'); + router.push('/map'); + return; + } + + // 画像登録が必要ない情報の場合は位置情報選択画面に遷移 + if (paramParser.currentData.isImageSkipped && !isEditing) { + router.push('/add/location'); + } + }, [paramParser.currentData]); + + const onClickNext = useCallback(() => { + const t = paramParser.currentData.editData?.type_srv; + + // 現在の入力情報を保存する。 + const newData = JSON.parse(JSON.stringify(paramParser.currentData)) as InputFormData; + + if (!isEditing && !newData.inputData?.gisData) { + // 新規登録の場合はベースとなるデータを作成 + newData.inputData.gisData = { + geometry: { + type: 'Point', + coordinates: [NaN, NaN] + }, + properties: {}, + type: 'Feature' + }; + } + const featureProps = newData.inputData?.gisData?.properties as Record<string, string>; + + newData.inputData.teethImageUrls = teethImages ?? []; + newData.inputData.otherImageUrls = otherImages ?? []; + + if (type === 'boar') { + featureProps['歯列写真ID'] = newTeethImageIds.join(','); + } + + featureProps[t === 'boar-2' ? '写真ID' : '画像ID'] = newOtherImageIds.join(','); + + paramParser.updateData(newData as InputFormData); + + // ページを遷移する + + if (isEditing) { + router.push('/edit/info'); + } else { + router.push('/add/location'); + } + }, [teethImages, otherImages, newOtherImageIds, newTeethImageIds, paramParser.currentData]); + + const onClickPrev = useCallback(() => { + if (isEditing) { + if (paramParser.currentData.isLocationSkipped) { + router.push( + { + pathname: '/detail', + query: { + type: paramParser.currentData.editData?.type, + type_srv: paramParser.currentData.editData?.type_srv, + id: paramParser.currentData.editData?.id, + version: paramParser.currentData.editData?.version + } + }, + '/detail' + ); + } else { + router.push('/edit/location'); + } + } else { + router.push('/add'); + } + }, [paramParser.currentData]); + + const onChangeTeethImages = (files: ImagewithLocation[]) => { + if (files.length === 0) { + setTeethImages(null); + } else { + setTeethImages(files); + } + }; + + const onChangeOtherImages = (files: ImagewithLocation[]) => { + if (files.length === 0) { + setOtherImages(null); + } else { + setOtherImages(files); + } + }; + + if (paramParser.isLoading) return <></>; + + return ( + <div> + <Header color={to_header_color(paramParser.currentData.dataType ?? '')}>{isEditing ? "登録画像編集" : "画像登録"}</Header> + <div className='mx-auto w-full max-w-[400px] bg-background py-3'> + <div className='mx-[15px] mt-2 text-justify'>画像を登録してください。</div> + <div className='mx-[15px] mt-2 text-justify'> + {type === 'boar' ? ( + <> + <div className="flex mt-2"> + <div>※ </div> + <div>歯列の写真は2枚まで登録できます。</div> + </div> + <div className="flex mt-2"> + <div>※ </div> + <div>その他の写真は8枚まで登録できます。</div> + </div> + <div className="flex mt-2"> + <div>※ </div> + <div>歯列の写真は必ず遠沈管番号がわかるように<br />撮影してください。</div> + </div> + <div className="flex mt-3"> + <div>※ </div> + <div> + <span className="font-bold">有害捕獲の場合:</span><br /> + ・検体採取個体の歯列写真 + </div> + </div> + <div className="flex"> + <div>※ </div> + <div> + <span className="font-bold">調査捕獲の場合:</span><br /> + ・検体採取個体の歯列写真<br /> + ・全捕獲個体のそれぞれ全体の写真 + </div> + </div> + </> + ) : ( + <>※ 画像は10枚まで登録できます。</> + )} + </div> + {type != 'boar' ? <></> : ( + <div className='box-border w-full px-[15px] py-2'> + <div className='text-justify text-lg font-bold text-text'> + 歯列の画像 + <ImageInput + max_count={type === 'boar' ? 2 : 10} + type={type} + single_file={false} + onChange={onChangeTeethImages} + objectURLs={teethImages == null ? undefined : teethImages} + imageIDs={isEditing ? currentServerTeethImageIds : undefined} + onServerImageDeleted={(list) => setNewTeethImageIds(list)} + /> + </div> + </div> + )} + <div className='box-border w-full px-[15px] py-2'> + <div className='text-justify text-lg font-bold text-text'> + {type == 'boar' ? 'その他の' : ''}画像 + <ImageInput + max_count={type === 'boar' ? 8 : 10} + type={type} + single_file={false} + onChange={onChangeOtherImages} + objectURLs={otherImages == null ? undefined : otherImages} + imageIDs={isEditing ? currentServerOtherImageIds : undefined} + onServerImageDeleted={(list) => setNewOtherImageIds(list)} + /> + </div> + </div> + </div> + <FooterAdjustment /> + <div className='fixed bottom-0 w-full'> + <Footer> + <RoundButton color='accent' onClick={onClickPrev.bind(this)}> + < 戻る + </RoundButton> + <RoundButton color='primary' onClick={onClickNext.bind(this)}> + 進む > + </RoundButton> + </Footer> + </div> + </div> + ); +}; + +export default ImageSelectorTemplate; \ No newline at end of file diff --git a/src/components/templates/newDataForm/interfaces.ts b/src/components/templates/newDataForm/interfaces.ts new file mode 100644 index 00000000..8d631938 --- /dev/null +++ b/src/components/templates/newDataForm/interfaces.ts @@ -0,0 +1,3 @@ +export interface InputFormTemplateCommonProps { + isEditing: boolean; +} \ No newline at end of file diff --git a/src/components/templates/newDataForm/locationSelector/index.tsx b/src/components/templates/newDataForm/locationSelector/index.tsx new file mode 100644 index 00000000..59d2cc4d --- /dev/null +++ b/src/components/templates/newDataForm/locationSelector/index.tsx @@ -0,0 +1,184 @@ +import { useRouter } from "next/router"; +import { InputFormTemplateCommonProps } from "../interfaces"; +import { InputFormData, useFormDataParser } from "../../../../utils/form-data"; +import FooterAdjustment from "../../../atomos/footerAdjustment"; +import RoundButton from "../../../atomos/roundButton"; +import Footer from "../../../organisms/footer"; +import Header from "../../../organisms/header"; +import { to_header_color } from "../../../../utils/header"; +import { useCallback, useEffect, useState } from "react"; +import { LatLngZoom, LatLngZoomCookie, Location } from "../../../organisms/mapBase/interface"; +import SelectionMap from "../../../organisms/selectionMap"; +import { parseCookies } from "nookies"; + +const LocationSelectorTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEditing }) => { + const router = useRouter(); + const paramParser = useFormDataParser(); + const type = paramParser.currentData.dataType; + + const [defaultLoc, setDefaultLoc] = useState<LatLngZoom | null>(null); + const [mapDiv, setMapDiv] = useState<JSX.Element | null>(null); + const [currentLoc, setCurrentLoc] = useState<LatLngZoom | null>(null); + + useEffect(() => { + if (!paramParser.currentData.dataType) { + alert('情報の取得に失敗しました。'); + router.push('/map'); + return; + } + + // 地図の初期位置の設定 + if (defaultLoc != null) return; + + // ズーム率はCookieから引っ張ってくる + let defaultZoom = 17; + const cookies = parseCookies(null); + const last_geo = cookies['last_geo']; + let last_geo_obj = null; + if (last_geo != null) { + last_geo_obj = JSON.parse(last_geo) as LatLngZoomCookie; + defaultZoom = last_geo_obj.zoom; + } + + const isLocationExists = (paramParser.currentData.inputData.gisData?.geometry?.coordinates ?? [null, null]).filter((e) => e !== null && !isNaN(e)).length == 2; + if (isLocationExists) { + // ポイント情報が既に存在する場合はその位置を初期位置とする + const coordinates = paramParser.currentData.inputData.gisData?.geometry?.coordinates ?? [35.39135, 136.722418]; + setDefaultLoc({ + isDefault: false, + zoom: defaultZoom, + lat: coordinates[1], + lng: coordinates[0], + }); + } else { + // ポイント情報が取得しない場合に初期位置を決定する。 + + // 画像に位置情報が存在する場合はその位置を初期位置とする + const imageArray = (paramParser.currentData.inputData.teethImageUrls ?? []).concat(paramParser.currentData.inputData.otherImageUrls ?? []); + let loc: Location | null = null; + + for (const imageInfo of imageArray) { + if (imageInfo.location != null) { + loc = imageInfo.location; + break; + } + } + + if(loc != null) { + setDefaultLoc({ + isDefault: false, + zoom: defaultZoom, + ...loc, + }); + } else { + // 歯列画像がない or 画像に位置情報がない + // → 最後に表示していたところを中心にする。ない場合はデフォルト位置 + if (last_geo_obj == null) { + setDefaultLoc({ + isDefault: true, + zoom: defaultZoom, + lat: 35.39135, + lng: 136.722418, + }); + } else { + setDefaultLoc({ + isDefault: false, + ...last_geo_obj, + }); + } + } + } + }, [paramParser.currentData, defaultLoc]); + + useEffect(() => { + if (defaultLoc == null) return; + setMapDiv( + <SelectionMap + location={defaultLoc} + onCenterChanged={(loc) => setCurrentLoc(loc)} + isLoaded={false} + />, + ); + }, [defaultLoc]); + + const onClickPrev = useCallback(() => { + if (isEditing) { + router.push( + { + pathname: '/detail', + query: { + type: paramParser.currentData.editData?.type, + type_srv: paramParser.currentData.editData?.type_srv, + id: paramParser.currentData.editData?.id, + version: paramParser.currentData.editData?.version + } + }, + '/detail' + ); + } else { + if (paramParser.currentData.isImageSkipped) { + router.push('/add'); + } else { + router.push('/add/image'); + } + } + }, [isEditing]); + + const onClickNext = useCallback(() => { + if (currentLoc == null) + return; + + // 現在の入力情報を保存する。 + const newData = JSON.parse(JSON.stringify(paramParser.currentData)) as InputFormData; + if (!isEditing && !newData.inputData?.gisData) { + // 新規登録の場合はベースとなるデータを作成 + newData.inputData.gisData = { + geometry: { + type: 'Point', + coordinates: [NaN, NaN] + }, + properties: {}, + type: 'Feature' + }; + } + + if (newData?.inputData?.gisData == null) + return; + + newData.inputData.gisData.geometry.coordinates = [currentLoc.lng, currentLoc.lat]; + + paramParser.updateData(newData as InputFormData); + + // ページを遷移する + if (isEditing) { + if (paramParser.currentData.isImageSkipped) { + // 作業日報など画像情報が存在しない場合は情報編集ページへ + router.push('/edit/info'); + } else { + router.push('/edit/image'); + } + } else { + router.push('/add/info'); + } + }, [currentLoc]); + + return ( + <div> + <Header color={to_header_color(type == null ? '' : type)}>位置情報{isEditing ? '編集' : '登録'}</Header> + {mapDiv} + <FooterAdjustment /> + <div className='fixed bottom-0 w-full'> + <Footer> + <RoundButton color='accent' onClick={onClickPrev.bind(this)}> + < 戻る + </RoundButton> + <RoundButton color='primary' onClick={onClickNext.bind(this)}> + 進む > + </RoundButton> + </Footer> + </div> + </div> + ); +}; + +export default LocationSelectorTemplate; \ No newline at end of file diff --git a/src/pages/add.tsx b/src/pages/add.tsx index 0d5c3941..92db264c 100644 --- a/src/pages/add.tsx +++ b/src/pages/add.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import AddTemplate from '../components/templates/addTemplate'; import { useRequireLogin } from '../hooks/useLogin'; +import AddTypeSelectorTemplate from '../components/templates/newDataForm/addTypeSelectorTemplate'; const AddPage: NextPage = () => { useRequireLogin(); return ( <> - <AddTemplate /> + <AddTypeSelectorTemplate /> </> ); }; diff --git a/src/pages/add/confirm.tsx b/src/pages/add/confirm.tsx index 6d0f765f..6c010438 100644 --- a/src/pages/add/confirm.tsx +++ b/src/pages/add/confirm.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import AddConfirmTemplate from '../../components/templates/addConfirmTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import DataConfirmTemplate from '../../components/templates/newDataForm/dataConfirmTemplate'; const AddConfirmPage: NextPage = () => { useRequireLogin(); return ( <> - <AddConfirmTemplate /> + <DataConfirmTemplate isEditing={false} /> </> ); }; diff --git a/src/pages/add/image.tsx b/src/pages/add/image.tsx index f96703d1..1d2c9cdc 100644 --- a/src/pages/add/image.tsx +++ b/src/pages/add/image.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import AddImageTemplate from '../../components/templates/addImageTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import ImageSelectorTemplate from '../../components/templates/newDataForm/imageSelector'; const AddImagePage: NextPage = () => { useRequireLogin(); return ( <> - <AddImageTemplate /> + <ImageSelectorTemplate isEditing={false} /> </> ); }; diff --git a/src/pages/add/info.tsx b/src/pages/add/info.tsx index 7d69360a..306af97e 100644 --- a/src/pages/add/info.tsx +++ b/src/pages/add/info.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import AddInfoTemplate from '../../components/templates/addInfoTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import CommonInfoInputTemplate from '../../components/templates/newDataForm/commonInfoInput'; const AddInfoPage: NextPage = () => { useRequireLogin(); return ( <> - <AddInfoTemplate /> + <CommonInfoInputTemplate isEditing={false} /> </> ); }; diff --git a/src/pages/add/location.tsx b/src/pages/add/location.tsx index 90cc0c1e..a1b325c6 100644 --- a/src/pages/add/location.tsx +++ b/src/pages/add/location.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import AddLocationTemplate from '../../components/templates/addLocationTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import LocationSelectorTemplate from '../../components/templates/newDataForm/locationSelector'; const AddLocationPage: NextPage = () => { useRequireLogin(); return ( <> - <AddLocationTemplate /> + <LocationSelectorTemplate isEditing={false} /> </> ); }; diff --git a/src/pages/edit/confirm.tsx b/src/pages/edit/confirm.tsx index 6674920c..7cecbaf9 100644 --- a/src/pages/edit/confirm.tsx +++ b/src/pages/edit/confirm.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import EditConfirmTemplate from '../../components/templates/editConfirmTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import DataConfirmTemplate from '../../components/templates/newDataForm/dataConfirmTemplate'; const EditConfirmPage: NextPage = () => { useRequireLogin(); return ( <> - <EditConfirmTemplate /> + <DataConfirmTemplate isEditing={true} /> </> ); }; diff --git a/src/pages/edit/image.tsx b/src/pages/edit/image.tsx index 08bfa747..bb149c35 100644 --- a/src/pages/edit/image.tsx +++ b/src/pages/edit/image.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import EditImageTemplate from '../../components/templates/editImageTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import ImageSelectorTemplate from '../../components/templates/newDataForm/imageSelector'; const EditImagePage: NextPage = () => { useRequireLogin(); return ( <> - <EditImageTemplate /> + <ImageSelectorTemplate isEditing={true} /> </> ); }; diff --git a/src/pages/edit/info.tsx b/src/pages/edit/info.tsx index 3b859217..37a57ba9 100644 --- a/src/pages/edit/info.tsx +++ b/src/pages/edit/info.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import EditInfoTemplate from '../../components/templates/editInfoTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import CommonInfoInputTemplate from '../../components/templates/newDataForm/commonInfoInput'; const EditInfoPage: NextPage = () => { useRequireLogin(); return ( <> - <EditInfoTemplate /> + <CommonInfoInputTemplate isEditing={true} /> </> ); }; diff --git a/src/pages/edit/location.tsx b/src/pages/edit/location.tsx index f12f287e..584af905 100644 --- a/src/pages/edit/location.tsx +++ b/src/pages/edit/location.tsx @@ -1,12 +1,12 @@ import { NextPage } from 'next'; -import EditLocationTemplate from '../../components/templates/editLocationTemplate'; import { useRequireLogin } from '../../hooks/useLogin'; +import LocationSelectorTemplate from '../../components/templates/newDataForm/locationSelector'; const EditLocationPage: NextPage = () => { useRequireLogin(); return ( <> - <EditLocationTemplate /> + <LocationSelectorTemplate isEditing={true} /> </> ); }; diff --git a/src/types/features.ts b/src/types/features.ts index e8ab198c..e5f9cd39 100644 --- a/src/types/features.ts +++ b/src/types/features.ts @@ -195,6 +195,12 @@ export interface ReportProps { 氏名: string; 画像ID: string; 入力者?: string; + 錯誤捕獲: string; + 止刺道具: string; + 捕獲補助: string; + 作業内容: string; + ワクチンNO: string; + 市町村字: string; } export interface ButanetsuFeature extends FeatureBase { diff --git a/src/utils/form-data.ts b/src/utils/form-data.ts new file mode 100644 index 00000000..2efb2415 --- /dev/null +++ b/src/utils/form-data.ts @@ -0,0 +1,93 @@ +import { useEffect, useMemo, useState } from "react"; +import { LayerType } from "./gis"; +import { parseCookies, setCookie } from "nookies"; +import { isObjectURLAvailable } from "./image"; +import { FeatureBase } from "../types/features"; +import { ImagewithLocation } from "../components/atomos/imageInput/interface"; + +export interface InputFormData { + dataType: LayerType; + isLocationSkipped: boolean; + isImageSkipped: boolean; + inputData: { + otherImageUrls?: ImagewithLocation[]; + teethImageUrls?: ImagewithLocation[]; + newImageIds?: string[]; + gisData?: FeatureBase; + } + editData?: { + id: string; + type: string | null; + type_srv: string | null; + version: string | null; + curImg: { + teeth: string[]; + other: string[]; + } + } +} + +export const useFormDataParser = () => { + const [isObjectURLChecked, setIsObjectURLChecked] = useState(false); + const [currentData, setCurrentData] = useState<InputFormData | Record<string, never>>(JSON.parse(parseCookies()['formData'] || '{}') ?? {}); + const [isLoading, setIsLoading] = useState(true); + const isDataExsiting = useMemo(() => Object.keys(currentData).length === 0, [currentData]); + + useEffect(() => { + console.log('updated', currentData); + }, [currentData]); + + useEffect(() => { + if (isObjectURLChecked) + return; + + const checkFunc = async () => { + const dataCopy = JSON.parse(JSON.stringify(currentData)) as InputFormData; + + if (currentData.inputData === undefined) { + setIsObjectURLChecked(true); + setIsLoading(false); + return; + } + + const checkKeys: (keyof typeof currentData.inputData)[] = [ + 'otherImageUrls', + 'teethImageUrls' + ]; + + for (const key of checkKeys) { + if (!currentData.inputData[key]) { + continue; + } + + const urls = currentData.inputData[key] as ImagewithLocation[]; + + const checkResults = await Promise.all(urls.map(async (url) => { + return await isObjectURLAvailable(url.objectURL); + })); + + // 無効だったものを弾いてデータに反映させる。 + const filteredUrls = urls.filter((_, i) => checkResults[i]); + dataCopy.inputData[key as 'otherImageUrls' | 'teethImageUrls'] = filteredUrls; + } + + updateData(dataCopy); + setIsObjectURLChecked(true); + setIsLoading(false); + }; + + checkFunc(); + }, [isObjectURLChecked]); + + const updateData = (data: InputFormData | null) => { + setCurrentData(data ?? {}); + setCookie(null, "formData", JSON.stringify(data), { path: '/' }); + }; + + return { + currentData, + isLoading, + isDataExsiting, + updateData + } as const; +}; \ No newline at end of file diff --git a/src/utils/image.ts b/src/utils/image.ts new file mode 100644 index 00000000..bb55e27b --- /dev/null +++ b/src/utils/image.ts @@ -0,0 +1,12 @@ +export const isObjectURLAvailable = async (url: string) => { + if (!url.startsWith('blob:')) { + return false; + } + + try { + const response = await fetch(url, { method: 'GET' }); + return response.ok; + } catch (e) { + return false; + } +}; \ No newline at end of file From 05dc1376874d79126af22f2ac7233bf1015d6ed4 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Thu, 16 Jan 2025 23:54:45 +0900 Subject: [PATCH 03/15] =?UTF-8?q?chore:=20=E3=82=B3=E3=83=9F=E3=83=83?= =?UTF-8?q?=E3=83=88=E6=BC=8F=E3=82=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/addConfirmTemplate/index.tsx | 233 ------------- .../templates/editConfirmTemplate/index.tsx | 324 ------------------ .../newDataForm/dataConfirmTemplate/index.tsx | 7 +- .../newDataForm/locationSelector/index.tsx | 3 +- src/pages/image.tsx | 2 +- 5 files changed, 7 insertions(+), 562 deletions(-) delete mode 100644 src/components/templates/addConfirmTemplate/index.tsx delete mode 100644 src/components/templates/editConfirmTemplate/index.tsx diff --git a/src/components/templates/addConfirmTemplate/index.tsx b/src/components/templates/addConfirmTemplate/index.tsx deleted file mode 100644 index 3fe6dfb3..00000000 --- a/src/components/templates/addConfirmTemplate/index.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { FeatureBase } from '../../../types/features'; -import { SERVER_URI } from '../../../utils/constants'; -import { getAccessToken } from '../../../utils/currentUser'; -import { to_header_color, to_header_title } from '../../../utils/header'; -import { alert, confirm } from '../../../utils/modal'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; -import RoundButton from '../../atomos/roundButton'; -import FeatureViewer from '../../organisms/featureViewer'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import { LatLngZoom } from '../../organisms/mapBase/interface'; - -const AddConfirmTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [type, setType] = useState<string | null>(null); - const [images, setImages] = useState<Record< - string, - ImagewithLocation | ImagewithLocation[] | null - > | null>(null); - const [, setLocation] = useState<LatLngZoom | null>(null); - const [feature, setFeature] = useState<FeatureBase | null>(null); - const [imageArray, setImageArray] = useState<ImagewithLocation[] | null>(null); - const [isLoading, setLoading] = useState(false); - - const uploadImages = async (): Promise<string[]> => { - const imgData = JSON.parse(router.query.images as string) as Record< - string, - null | ImagewithLocation | ImagewithLocation[] - >; - if (imgData == null || imgData.otherImages == null || !Array.isArray(imgData.otherImages)) { - // ない場合は空の配列 - return []; - } - const imgArr = imgData.otherImages as ImagewithLocation[]; - // 1枚も画像がなければ空の配列を返す - if (imgArr.length === 0) { - return []; - } - - // 1枚以上画像があれば,アップロード→idを返す - const ids = []; - // 送信用データ生成 - const body = new FormData(); - for (let i = 0; i < imgArr.length; i++) { - const blob = await fetch(imgArr[i].objectURL).then((r) => r.blob()); - body.append('files[]', blob); - } - const url = SERVER_URI + '/Image/AddImage?type=' + router.query.type; - const req = { - method: 'POST', - body: body, - headers: { - 'X-Access-Token': getAccessToken(), - }, - }; - - const r = await fetch(url, req); - const json = await r.json(); - if (json['status'] == 200) { - const resList = json['results'] as Record<string, unknown>[]; - for (let i = 0; i < resList.length; i++) { - const element = resList[i]; - ids.push(element['id'] as string); - } - } else { - console.error(json['reason']); - } - - return ids; - }; - - const uploadTeethImage = async (): Promise<string> => { - const imgData = JSON.parse(router.query.images as string) as Record< - string, - null | ImagewithLocation | ImagewithLocation[] - >; - if (imgData == null || imgData.teethImage == null) { - // ない場合は空の配列 - return ''; - } - const img = imgData.teethImage as ImagewithLocation; - - // 送信用データ生成 - const body = new FormData(); - const blob = await fetch(img.objectURL).then((r) => r.blob()); - body.append('files[]', blob); - const url = SERVER_URI + '/Image/AddImage?type=' + router.query.type; - const req = { - method: 'POST', - body: body, - headers: { - 'X-Access-Token': getAccessToken(), - }, - }; - - const r = await fetch(url, req); - const json = await r.json(); - if (json['status'] == 200) { - const resList = json['results'] as Record<string, unknown>[]; - for (let i = 0; i < resList.length; i++) { - const element = resList[i]; - return element['id'] as string; - } - } else { - console.error(json['reason']); - } - - return ''; - }; - - useEffect(() => { - if ( - router.query.images == null || - router.query.type == null || - router.query.location == null || - router.query.feature == null - ) { - alert('情報の取得に失敗しました。\nもう一度やり直してください。'); - router.push('/add'); - return; - } - - setType(router.query.type as string); - setImages(JSON.parse(router.query.images as string)); - setLocation(JSON.parse(router.query.location as string)); - setFeature(JSON.parse(router.query.feature as string)); - }, []); - - useEffect(() => { - if (imageArray == null && images != null) { - const baseArr = - images == null || images.otherImages == null - ? [] - : (images.otherImages as ImagewithLocation[]); - if (images.teethImage != null) { - baseArr.unshift(images.teethImage as ImagewithLocation); - } - setImageArray(baseArr); - } - }, [imageArray, images]); - - const onClickNext = async () => { - if (await confirm('この内容でよろしいですか?')) { - // 画像のアップロード - setLoading(true); - const img_ids = await uploadImages(); - let img_teeth = ''; - if (router.query.type === 'boar') { - // 捕獲イノシシ情報の場合は歯列画像も上げる - img_teeth = await uploadTeethImage(); - } - const img_ids_feature = img_ids.join(','); - - const feature = JSON.parse(router.query.feature as string) as FeatureBase; - if (router.query.type === 'boar') { - (feature.properties as Record<string, unknown>)['写真ID'] = img_ids_feature; - (feature.properties as Record<string, unknown>)['歯列写真ID'] = img_teeth; - } else { - (feature.properties as Record<string, unknown>)['画像ID'] = img_ids_feature; - } - - const res = await fetch(SERVER_URI + '/Features/AddFeature', { - method: 'POST', - headers: { - 'X-Access-Token': getAccessToken(), - }, - body: JSON.stringify({ - type: router.query.type, - feature: feature, - }), - }); - - const json = await res.json(); - if (res.status === 200) { - await alert('登録が完了しました。\nご協力ありがとうございました。'); - setLoading(false); - router.push('/map'); - } else { - console.error(json['error']); - await alert('エラーが発生しました。\n' + json['error']); - } - setLoading(false); - } - }; - - const onClickPrev = () => { - router.push( - { - pathname: '/add-old/info', - query: { - type: router.query.type, - images: router.query.images, - location: router.query.location, - feature: router.query.feature, - }, - }, - '/add-old/info', - ); - }; - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}> - {to_header_title(type == null ? '' : type)}登録 - </Header> - <div className='mx-auto w-full max-w-[400px] bg-background py-3'> - <div className='mx-[15px] mt-2 text-justify'>情報に不備がないかご確認ください。</div> - </div> - <FeatureViewer - featureInfo={feature} - type={type} - objectURLs={imageArray?.map((f) => f.objectURL)} - confirm={true} - /> - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='danger' onClick={onClickNext.bind(this)} disabled={isLoading}> - {isLoading ? '読み込み中...' : '登録 >'} - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default AddConfirmTemplate; diff --git a/src/components/templates/editConfirmTemplate/index.tsx b/src/components/templates/editConfirmTemplate/index.tsx deleted file mode 100644 index 3ee2f59d..00000000 --- a/src/components/templates/editConfirmTemplate/index.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { BoarFeatureV2, FeatureBase } from '../../../types/features'; -import { to_header_color, to_header_title } from '../../../utils/header'; -import FooterAdjustment from '../../atomos/footerAdjustment'; -import { alert, confirm } from '../../../utils/modal'; -import { ImagewithLocation } from '../../atomos/imageInput/interface'; -import RoundButton from '../../atomos/roundButton'; -import FeatureViewer from '../../organisms/featureViewer'; -import Footer from '../../organisms/footer'; -import Header from '../../organisms/header'; -import { SERVER_URI } from '../../../utils/constants'; -import { getAccessToken } from '../../../utils/currentUser'; - -const EditConfirmTemplate: React.FunctionComponent = () => { - const router = useRouter(); - const [isLoading, setLoading] = useState(false); - const [type, setType] = useState<string | null>(''); - const [images, setImages] = useState<Record< - string, - ImagewithLocation | ImagewithLocation[] | null - > | null>(null); - const [feature, setFeature] = useState<FeatureBase | null>(null); - const [imageArray, setImageArray] = useState<ImagewithLocation[] | null>(null); - - const [serverImages, setServerImages] = useState<string[] | null>(null); - - useEffect(() => { - if ( - router.query.type == null || - router.query.id == null || - router.query.version == null || - router.query.detail == null || - router.query.location == null || - router.query.images == null || - router.query.serverImages == null - ) { - alert('情報の取得に失敗しました。'); - router.push('/map'); - return; - } - - let t = router.query.type_srv as string; - if (t === 'boar-1') { - t = 'boar-old'; - } else if (t === 'boar-2') { - t = 'boar'; - } - - setType(t); - setImages(JSON.parse(router.query.images as string)); - const f = router.query.detail != null ? JSON.parse(router.query.detail as string) : null; - setFeature(f); - - const srvImgs = JSON.parse(router.query.serverImages as string); - const imgs = srvImgs['newServerImages'] as string[]; - if (t === 'boar' && !(srvImgs.isDeleteTeethImage as boolean)) { - imgs.unshift((f as BoarFeatureV2).properties.歯列写真ID); - } - - setServerImages(imgs.filter((e) => e)); - }, []); - - useEffect(() => { - if (imageArray == null && images != null) { - const baseArr = - images == null || images.otherImages == null - ? [] - : (images.otherImages as ImagewithLocation[]); - if (images.teethImage != null) { - baseArr.unshift(images.teethImage as ImagewithLocation); - } - setImageArray(baseArr); - } - }, [imageArray, images]); - - const uploadImages = async (): Promise<string[]> => { - const imgData = JSON.parse(router.query.images as string) as Record< - string, - null | ImagewithLocation | ImagewithLocation[] - >; - if (imgData == null || imgData.otherImages == null || !Array.isArray(imgData.otherImages)) { - // ない場合は空の配列 - return []; - } - const imgArr = imgData.otherImages as ImagewithLocation[]; - // 1枚も画像がなければ空の配列を返す - if (imgArr.length === 0) { - return []; - } - - // 1枚以上画像があれば,アップロード→idを返す - const ids = []; - // 送信用データ生成 - const body = new FormData(); - for (let i = 0; i < imgArr.length; i++) { - const blob = await fetch(imgArr[i].objectURL).then((r) => r.blob()); - body.append('files[]', blob); - } - const url = - SERVER_URI + - '/Image/AddImage?type=' + - ((router.query.type_srv as string).startsWith('boar-') ? 'boar' : router.query.type_srv); - const req = { - method: 'POST', - body: body, - headers: { - 'X-Access-Token': getAccessToken(), - }, - }; - - const r = await fetch(url, req); - const json = await r.json(); - if (json['status'] == 200) { - const resList = json['results'] as Record<string, unknown>[]; - for (let i = 0; i < resList.length; i++) { - const element = resList[i]; - ids.push(element['id'] as string); - } - } else { - console.error(json['reason']); - } - - return ids; - }; - - const uploadTeethImage = async (): Promise<string> => { - const imgData = JSON.parse(router.query.images as string) as Record< - string, - null | ImagewithLocation | ImagewithLocation[] - >; - if (imgData == null || imgData.teethImage == null) { - // ない場合は空の配列 - return ''; - } - const img = imgData.teethImage as ImagewithLocation; - - // 送信用データ生成 - const body = new FormData(); - const blob = await fetch(img.objectURL).then((r) => r.blob()); - body.append('files[]', blob); - const url = - SERVER_URI + - '/Image/AddImage?type=' + - ((router.query.type_srv as string).startsWith('boar-') ? 'boar' : router.query.type_srv); - const req = { - method: 'POST', - body: body, - headers: { - 'X-Access-Token': getAccessToken(), - }, - }; - - const r = await fetch(url, req); - const json = await r.json(); - if (json['status'] == 200) { - const resList = json['results'] as Record<string, unknown>[]; - for (let i = 0; i < resList.length; i++) { - const element = resList[i]; - return element['id'] as string; - } - } else { - console.error(json['reason']); - } - - return ''; - }; - - const onClickPrev = () => { - router.push( - { - pathname: '/edit-old/info', - query: { - type: router.query.type, - type_srv: router.query.type_srv, - id: router.query.id, - version: router.query.version, - detail: router.query.detail, - location: router.query.location, - images: router.query.images, - serverImages: router.query.serverImages, - }, - }, - '/edit-old/info', - ); - }; - - const deleteImage = (id: string) => { - return new Promise<void>((resolve, reject) => { - try { - const data = new FormData(); - data.append('id', id); - const options = { - method: 'POST', - body: data, - headers: { - Accept: 'application/json', - 'X-Access-Token': getAccessToken(), - }, - }; - fetch(`${SERVER_URI}/Image/DeleteImage`, options) - .then((res) => { - if (res.status === 200 || res.status === 404) { - resolve(); - } else { - return res.json(); - } - }) - .then((json) => reject(json['reason'])) - .catch((e) => reject(e)); - } catch (e) { - reject(e); - } - }); - }; - - const onClickNext = async () => { - if (await confirm('この内容でよろしいですか?')) { - // 削除ボタンを押された画像の削除 - const srvImgs = JSON.parse(router.query.serverImages as string); - const feature = JSON.parse(router.query.detail as string) as FeatureBase; - if (router.query.type_srv === 'boar-2' && srvImgs.isDeleteTeethImage) { - // 捕獲イノシシ情報の場合は歯列画像も削除する - await deleteImage((feature.properties as Record<string, unknown>)['歯列写真ID'] as string); - } - - const currentList = ( - (feature.properties as Record<string, unknown>)[ - router.query.type_srv === 'boar-2' ? '写真ID' : '画像ID' - ] as string - ) - .split(',') - .filter((e) => e); - const remainList = currentList.filter((v) => - (srvImgs.newServerImages as string[]).includes(v), - ); - const deleteList = currentList.filter((v) => !remainList.includes(v)); - await Promise.all(deleteList.map((id) => deleteImage(id))); - - // 画像のアップロード - setLoading(true); - const img_ids = await uploadImages(); - let img_teeth = ''; - if (router.query.type_srv === 'boar-2') { - // 捕獲イノシシ情報の場合は歯列画像も上げる - if (!srvImgs.isDeleteTeethImage) { - img_teeth = (feature.properties as Record<string, unknown>)['歯列写真ID'] as string; - } - const t = await uploadTeethImage(); - if (t != '') img_teeth = t; - } - const register_imgs = remainList.concat(img_ids).filter((e) => e); - const img_ids_feature = register_imgs.join(','); - - if (router.query.type_srv === 'boar-2') { - (feature.properties as Record<string, unknown>)['写真ID'] = img_ids_feature; - (feature.properties as Record<string, unknown>)['歯列写真ID'] = img_teeth; - } else { - (feature.properties as Record<string, unknown>)['画像ID'] = img_ids_feature; - } - - console.debug({ - router: router, - body: { - type: router.query.type_srv, - feature: feature, - } - }); - - const res = await fetch(SERVER_URI + '/Features/UpdateFeature', { - method: 'POST', - headers: { - 'X-Access-Token': getAccessToken(), - }, - body: JSON.stringify({ - type: router.query.type_srv, - feature: feature, - }), - }); - - const json = await res.json(); - if (res.status === 200) { - await alert('登録が完了しました。\nご協力ありがとうございました。'); - setLoading(false); - router.push('/map'); - } else { - console.error(json['error']); - await alert('エラーが発生しました。\n' + json['error']); - } - setLoading(false); - } - }; - - return ( - <div> - <Header color={to_header_color(type == null ? '' : type)}> - {to_header_title(type == null ? '' : type)}編集 - </Header> - <div className='mx-auto w-full max-w-[400px] bg-background py-3'> - <div className='mx-[15px] mt-2 text-justify'>情報に不備がないかご確認ください。</div> - </div> - <FeatureViewer - featureInfo={feature} - type={type} - objectURLs={imageArray?.map((f) => f.objectURL)} - imageIDs={serverImages == null ? undefined : serverImages} - confirm={true} - /> - <FooterAdjustment /> - <div className='fixed bottom-0 w-full'> - <Footer> - <RoundButton color='accent' onClick={onClickPrev.bind(this)}> - < 戻る - </RoundButton> - <RoundButton color='danger' onClick={onClickNext.bind(this)} disabled={isLoading}> - {isLoading ? '読み込み中...' : '登録 >'} - </RoundButton> - </Footer> - </div> - </div> - ); -}; - -export default EditConfirmTemplate; diff --git a/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx b/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx index dd14054a..5d92317f 100644 --- a/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx +++ b/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx @@ -8,7 +8,7 @@ import Header from "../../../organisms/header"; import { InputFormTemplateCommonProps } from "../interfaces"; import { InputFormData, useFormDataParser } from "../../../../utils/form-data"; import { useRouter } from "next/router"; -import { BoarFeatureV2, FeatureBase } from "../../../../types/features"; +import { FeatureBase } from "../../../../types/features"; import { alert, confirm } from "../../../../utils/modal"; import { getAccessToken } from "../../../../utils/currentUser"; import { SERVER_URI } from "../../../../utils/constants"; @@ -126,14 +126,14 @@ const DataConfirmTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEditing const teethImageIds = (await Promise.all((paramParser.currentData.inputData.teethImageUrls || []).map(e => uploadImage(e.objectURL)))).filter(e => e != null); const currentIds = (newData.inputData.gisData.properties as Record<string, string>)["歯列写真ID"].split(","); - const newIds = currentIds.concat(teethImageIds).filter(e => e); + const newIds = currentIds.concat(teethImageIds as string[]).filter(e => e); (newData.inputData.gisData.properties as Record<string, string>)['歯列写真ID'] = newIds.join(','); } // 画像をアップロードしてFeatureにセット const otherImageIds = (await Promise.all((paramParser.currentData.inputData.otherImageUrls || []).map(e => uploadImage(e.objectURL)))).filter(e => e != null); const currentIds = (newData.inputData.gisData.properties as Record<string, string>)[t == "boar" ? "写真ID" : "画像ID"].split(","); - const newIds = currentIds.concat(otherImageIds).filter(e => e); + const newIds = currentIds.concat(otherImageIds as string[]).filter(e => e); (newData.inputData.gisData.properties as Record<string, string>)[t == "boar" ? "写真ID" : "画像ID"] = newIds.join(','); @@ -166,6 +166,7 @@ const DataConfirmTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEditing if (res.status === 200) { await alert('登録が完了しました。\nご協力ありがとうございました。'); + paramParser.updateData(null); setIsLoading(false); router.push('/map'); } else { diff --git a/src/components/templates/newDataForm/locationSelector/index.tsx b/src/components/templates/newDataForm/locationSelector/index.tsx index 59d2cc4d..55d67aed 100644 --- a/src/components/templates/newDataForm/locationSelector/index.tsx +++ b/src/components/templates/newDataForm/locationSelector/index.tsx @@ -40,7 +40,8 @@ const LocationSelectorTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEd defaultZoom = last_geo_obj.zoom; } - const isLocationExists = (paramParser.currentData.inputData.gisData?.geometry?.coordinates ?? [null, null]).filter((e) => e !== null && !isNaN(e)).length == 2; + const coordinates = paramParser.currentData.inputData.gisData?.geometry?.coordinates ?? [NaN, NaN]; + const isLocationExists = coordinates.filter((e) => !isNaN(e)).length == 2; if (isLocationExists) { // ポイント情報が既に存在する場合はその位置を初期位置とする const coordinates = paramParser.currentData.inputData.gisData?.geometry?.coordinates ?? [35.39135, 136.722418]; diff --git a/src/pages/image.tsx b/src/pages/image.tsx index 0e5c655f..b4983a29 100644 --- a/src/pages/image.tsx +++ b/src/pages/image.tsx @@ -1,6 +1,6 @@ import { NextPage } from "next"; import { useRouter } from "next/router"; -import React, { useCallback, useEffect } from "react"; +import React, { useEffect } from "react"; import { SERVER_URI } from "../utils/constants"; import { getAccessToken } from "../utils/currentUser"; From 313587b84629bd608c900ae5dec9bde2005f3fa1 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Thu, 16 Jan 2025 23:56:27 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20=E6=AD=AF=E5=88=97=E7=94=BB?= =?UTF-8?q?=E5=83=8F=E3=82=92=E8=A4=87=E6=95=B0=E8=A1=A8=E7=A4=BA=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/organisms/boarInfov2View/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/organisms/boarInfov2View/index.tsx b/src/components/organisms/boarInfov2View/index.tsx index 994233d9..c0a43909 100644 --- a/src/components/organisms/boarInfov2View/index.tsx +++ b/src/components/organisms/boarInfov2View/index.tsx @@ -51,8 +51,8 @@ const BoarInfov2View: React.FunctionComponent<BoarInfov2ViewProps> = ({ return imageIDs; } else { const d = detail.properties.写真ID.split(','); - d.unshift(detail.properties.歯列写真ID); - return d.filter((e) => e); + const d2 = detail.properties.歯列写真ID.split(','); + return d2.concat(d).filter((e) => e); } }; From 84915c453870fbdfda56b05fc9f238e714246285 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Fri, 17 Jan 2025 01:43:54 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20=E4=BD=9C=E6=A5=AD=E6=97=A5?= =?UTF-8?q?=E5=A0=B1=E3=81=AE=E9=A0=85=E7=9B=AE=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/atomos/reportBInput/index.tsx | 88 +++++++ .../atomos/reportBInput/interface.ts | 7 + .../atomos/workDetailInput/index.tsx | 233 ++++++++++++++++++ .../atomos/workDetailInput/interface.ts | 7 + .../organisms/reportInfoForm/index.tsx | 72 +++++- .../templates/detailTemplate/index.tsx | 2 + 6 files changed, 400 insertions(+), 9 deletions(-) create mode 100644 src/components/atomos/reportBInput/index.tsx create mode 100644 src/components/atomos/reportBInput/interface.ts create mode 100644 src/components/atomos/workDetailInput/index.tsx create mode 100644 src/components/atomos/workDetailInput/interface.ts diff --git a/src/components/atomos/reportBInput/index.tsx b/src/components/atomos/reportBInput/index.tsx new file mode 100644 index 00000000..79f80aad --- /dev/null +++ b/src/components/atomos/reportBInput/index.tsx @@ -0,0 +1,88 @@ +import React, { useMemo } from "react"; +import TextInput from "../TextInput"; +import { ReportBInputProps } from "./interface"; + +const ReportBInput: React.FC<ReportBInputProps> = (props) => { + const parsedToolValue = useMemo(() => { + // データ構造は下記の通り + // (電気とめさし器(チェック時))/(銃(チェック時))/(その他(チェック時)) + // その他の場合の内容 + const data = { + tool: { + elec: false, + gun: false, + other: false + }, + other_tool: '' + }; + + if (!props.toolDefaultValue) { + return data; + } + + const lines = props.toolDefaultValue.split('\n'); + const tool = (lines[0] || '').split('/'); + data.tool.elec = tool[0] === '電気とめさし器'; + data.tool.gun = tool[1] === '銃'; + data.tool.other = tool[2] === 'その他'; + + data.other_tool = lines[1]; + + return data; + }, [props.toolDefaultValue]); + + const [isOther, setIsOther] = React.useState(parsedToolValue.tool.other); + + return ( + <div className='m-[15px]'> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + 作業日報B + {props.required ? <span className='ml-[5px] font-bold text-danger'>*</span> : <></>} + </div> + {props.error != null ? ( + <div className='-mt-[5px] mb-[5px] w-full text-sm text-danger'>{props.error}</div> + ) : ( + <></> + )} + <div>※ その他の項目は自動的に計算されます。</div> + <div> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + 捕獲作業を手伝った者の氏名 + </div> + <TextInput + type="text" + id={props.id + "_helper"} + defaultValue={props.helperDefaultValue || ''} + /> + </div> + <div> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + とめさしの道具 + </div> + <div className="flex mb-2 flex-wrap"> + <div className="mr-4 mb-2"> + <input type="checkbox" id={`${props.id}_elec`} className="scale-[2] w-7 mr-1" defaultChecked={parsedToolValue.tool.elec} /> + <label htmlFor={`${props.id}_elec`} className="text-lg" >電気とめさし器</label> + </div> + <div className="mr-4 mb-2"> + <input type="checkbox" id={`${props.id}_gun`} className="scale-[2] w-7 mr-1" defaultChecked={parsedToolValue.tool.gun} /> + <label htmlFor={`${props.id}_gun`} className="text-lg" >銃</label> + </div> + <div> + <input type="checkbox" id={`${props.id}_other`} className="scale-[2] w-7 mr-1" defaultChecked={parsedToolValue.tool.other} onChange={(e) => setIsOther(e.target.checked)}/> + <label htmlFor={`${props.id}_other`} className="text-lg" >その他</label> + </div> + </div> + {isOther ? ( + <TextInput + type="text" + id={props.id + "_other_tool"} + defaultValue={parsedToolValue.other_tool} + /> + ) : <></>} + </div> + </div> + ); +}; + +export default ReportBInput; \ No newline at end of file diff --git a/src/components/atomos/reportBInput/interface.ts b/src/components/atomos/reportBInput/interface.ts new file mode 100644 index 00000000..1ff44784 --- /dev/null +++ b/src/components/atomos/reportBInput/interface.ts @@ -0,0 +1,7 @@ +export interface ReportBInputProps { + id: string; + helperDefaultValue?: string; + toolDefaultValue?: string; + required?: boolean; + error?: string; +} \ No newline at end of file diff --git a/src/components/atomos/workDetailInput/index.tsx b/src/components/atomos/workDetailInput/index.tsx new file mode 100644 index 00000000..69c9c381 --- /dev/null +++ b/src/components/atomos/workDetailInput/index.tsx @@ -0,0 +1,233 @@ +import React, { useMemo, useState } from "react"; +import { WorkDetailInputProps } from "./interface"; +import TextInput from "../TextInput"; +import InfoInput from "../../molecules/infoInput"; + +const WorkDetailInput: React.FC<WorkDetailInputProps> = (props) => { + const parsedDefaultValue = useMemo(() => { + const data = { + trap: { + placed: 0, + removed: 0 + }, + capture: false, + crawl: false, + capture_type: { + own: false, + help: false, + mistake: false + } + }; + + if (!props.workDefaultValue) { + return data; + } + + + // データ構造は下記の通り (スラッシュは未チェック時も挿入される) + // (わな設置)/(撤去)/(見回り(チェック時))/(捕獲(チェック時)) + // 自身のわな/捕獲手伝い/錯誤捕獲 + + const lines = props.workDefaultValue.split('\n'); + const trap = (lines[0] || '').split('/'); + data.trap.placed = isNaN(parseInt(trap[0])) ? 0 : parseInt(trap[0]); + data.trap.removed = isNaN(parseInt(trap[1])) ? 0 : parseInt(trap[1]); + if (trap[2] === '見回り') { + data.crawl = true; + } + if (trap[3] === '捕獲') { + data.capture = true; + } + + const capture = (lines[1] || '').split('/'); + data.capture_type.own = capture[0] === '自身のわな'; + data.capture_type.help = capture[1] === '捕獲手伝い'; + data.capture_type.mistake = capture[2] === '錯誤捕獲'; + + return data; + }, [props.workDefaultValue]); + + const parsedMistakeValue = useMemo(() => { + const data = { + trap_type: '', + head_count: 0, + response: '', + animal_type: { + deer: false, + serow: false, + boar: false, + other: false + }, + animal_other: '' + }; + + if (!props.mistakeDefaultValue) { + return data; + } + + // データ構造は下記の通り + // わなの種類 + // 頭数 + // 対応 + // (ニホンジカ(チェック時))/(カモシカ(チェック時))/(ツキノワグマ(チェック時))/(その他(チェック時)) + // その他の場合の獣種 + + const lines = props.mistakeDefaultValue.split('\n'); + data.trap_type = (lines[0] || ''); + data.head_count = isNaN(parseInt(lines[1])) ? 0 : parseInt(lines[1]); + data.response = (lines[2] || ''); + + const animal = (lines[3] || '').split('/'); + data.animal_type.deer = animal[0] === 'ニホンジカ'; + data.animal_type.serow = animal[1] === 'カモシカ'; + data.animal_type.boar = animal[2] === 'ツキノワグマ'; + data.animal_type.other = animal[3] === 'その他'; + + data.animal_other = lines[4]; + + return data; + }, [props.mistakeDefaultValue]); + + const [isCaptureSet, setIsCaptureSet] = useState(parsedDefaultValue.capture); + const [isMistake, setIsMistake] = useState(parsedDefaultValue.capture_type.mistake); + const [isOther, setIsOther] = useState(parsedMistakeValue.animal_type.other); + + return ( + <div className='m-[15px]'> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + 作業内容 + {props.required ? <span className='ml-[5px] font-bold text-danger'>*</span> : <></>} + </div> + {props.error != null ? ( + <div className='-mt-[5px] mb-[5px] w-full text-sm text-danger'>{props.error}</div> + ) : ( + <></> + )} + + <div className="mb-2"> + <div>わな設置</div> + <div className="flex"> + <div className="flex-1"> + <TextInput + type="number" + id={props.id + "_placed"} + defaultValue={`${parsedDefaultValue.trap.placed}`} + /> + </div> + <div className="flex items-center mx-1 font-bold text-lg">基</div> + </div> + </div> + + <div className="mb-2"> + <div>わな撤去</div> + <div className="flex"> + <div className="flex-1"> + <TextInput + type="number" + id={props.id + "_removed"} + defaultValue={`${parsedDefaultValue.trap.removed}`} + /> + </div> + <div className="flex items-center mx-1 font-bold text-lg">基</div> + </div> + </div> + + <div className="flex mb-2 flex-wrap"> + <div> + <input type="checkbox" id={`${props.id}_crawl`} className="scale-[2] w-7 mr-1" defaultChecked={parsedDefaultValue.crawl} /> + <label htmlFor={`${props.id}_crawl`} className="text-lg">見回り</label> + </div> + <div className="ml-4"> + <input type="checkbox" id={`${props.id}_capture`} className="scale-[2] w-7 mr-1" defaultChecked={parsedDefaultValue.capture} onChange={(e) => setIsCaptureSet(e.target.checked)}/> + <label htmlFor={`${props.id}_capture`} className="text-lg">捕獲</label> + </div> + </div> + + {isCaptureSet ? ( + <> + <div> + <div>※ 捕獲を行った場合には以下の項目もチェックしてください。</div> + <div className="flex mb-2 flex-wrap"> + <div className="mr-4 mb-2"> + <input type="checkbox" id={`${props.id}_own`} className="scale-[2] w-7 mr-1" defaultChecked={parsedDefaultValue.capture_type.own} /> + <label htmlFor={`${props.id}_own`} className="text-lg" >自身の罠で捕獲</label> + </div> + <div className="mr-4 mb-2"> + <input type="checkbox" id={`${props.id}_help`} className="scale-[2] w-7 mr-1" defaultChecked={parsedDefaultValue.capture_type.help} /> + <label htmlFor={`${props.id}_help`} className="text-lg" >捕獲手伝い</label> + </div> + <div> + <input type="checkbox" id={`${props.id}_mistake`} className="scale-[2] w-7 mr-1" defaultChecked={parsedDefaultValue.capture_type.mistake} onChange={(e) => setIsMistake(e.target.checked)}/> + <label htmlFor={`${props.id}_mistake`} className="text-lg" >錯誤捕獲</label> + </div> + </div> + </div> + {isMistake ? ( + <div> + <div>※ 錯誤捕獲を行った場合には以下の項目も入力してください。</div> + <InfoInput + title='罠の種類' + type='select' + id={props.id + "_trap_type"} + options={['くくりわな', '箱わな (脱出口有り)', '箱わな (脱出口無し)', '囲いわな']} + defaultValue={parsedMistakeValue.trap_type} + /> + + <div className="px-4"> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + 獣種 + </div> + <div className="flex mb-2 flex-wrap"> + <div className="mr-4 mb-2"> + <input type="checkbox" id={`${props.id}_deer`} className="scale-[2] w-7 mr-1" defaultChecked={parsedMistakeValue.animal_type.deer} /> + <label htmlFor={`${props.id}_deer`} className="text-lg" >ニホンジカ</label> + </div> + <div className="mr-4 mb-2"> + <input type="checkbox" id={`${props.id}_serow`} className="scale-[2] w-7 mr-1" defaultChecked={parsedMistakeValue.animal_type.serow} /> + <label htmlFor={`${props.id}_serow`} className="text-lg" >カモシカ</label> + </div> + <div className="mr-4 mb-2"> + <input type="checkbox" id={`${props.id}_boar`} className="scale-[2] w-7 mr-1" defaultChecked={parsedMistakeValue.animal_type.boar} /> + <label htmlFor={`${props.id}_boar`} className="text-lg" >ツキノワグマ</label> + </div> + <div> + <input type="checkbox" id={`${props.id}_other`} className="scale-[2] w-7 mr-1" defaultChecked={parsedMistakeValue.animal_type.other} onChange={(e) => setIsOther(e.target.checked)}/> + <label htmlFor={`${props.id}_other`} className="text-lg" >その他</label> + </div> + </div> + {isOther ? ( + <TextInput + type="text" + id={props.id + "_other_animal"} + defaultValue={`${parsedMistakeValue.animal_other}`} + /> + ) : <></>} + </div> + + <div className="px-4"> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + 頭数 + </div> + <TextInput + type="number" + id={props.id + "_head"} + defaultValue={`${parsedMistakeValue.head_count}`} + /> + </div> + + <InfoInput + title='対応' + type='select' + id={props.id + "_response"} + options={['放獣', '市町村に依頼して捕獲']} + defaultValue={parsedMistakeValue.response} + /> + </div> + ) : <></>} + </> + ) : <></>} + </div> + ); +}; + +export default WorkDetailInput; \ No newline at end of file diff --git a/src/components/atomos/workDetailInput/interface.ts b/src/components/atomos/workDetailInput/interface.ts new file mode 100644 index 00000000..922a14cf --- /dev/null +++ b/src/components/atomos/workDetailInput/interface.ts @@ -0,0 +1,7 @@ +export interface WorkDetailInputProps { + id: string; + workDefaultValue?: string; + mistakeDefaultValue?: string; + required?: boolean; + error?: string; +} \ No newline at end of file diff --git a/src/components/organisms/reportInfoForm/index.tsx b/src/components/organisms/reportInfoForm/index.tsx index 05c4c59a..7463acd7 100644 --- a/src/components/organisms/reportInfoForm/index.tsx +++ b/src/components/organisms/reportInfoForm/index.tsx @@ -8,6 +8,8 @@ import WorkTimeInput from '../../atomos/workTimeInput'; import InfoInput from '../../molecules/infoInput'; import { FeatureEditorHandler } from '../featureEditor/interface'; import { ReportInfoFormProps } from './interface'; +import WorkDetailInput from '../../atomos/workDetailInput'; +import ReportBInput from '../../atomos/reportBInput'; const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProps>( function InfoForm(props, ref) { @@ -29,10 +31,53 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp const report = form.report.value as string; const note = form.note.value as string; + const city = (document.getElementById('city') as HTMLInputElement).value as string; + const vaccine_no = (document.getElementById('vaccine_no') as HTMLInputElement).value as string; + + // 作業内容の組み立て + const workdetail_placed = (document.getElementById('workdetail_placed') as HTMLInputElement).value; + const workdetail_removed = (document.getElementById('workdetail_removed') as HTMLInputElement).value; + + const workdetail_crawl = (document.getElementById('workdetail_crawl') as HTMLInputElement).checked; + const workdetail_capture = (document.getElementById('workdetail_capture') as HTMLInputElement).checked; + + const workdetail_own = workdetail_capture ? (document.getElementById('workdetail_own') as HTMLInputElement).checked : false; + const workdetail_help = workdetail_capture ? (document.getElementById('workdetail_help') as HTMLInputElement).checked : false; + const workdetail_mistake = workdetail_capture ? (document.getElementById('workdetail_mistake') as HTMLInputElement).checked : false; + + const workdetail = `${workdetail_placed}/${workdetail_removed}/${workdetail_crawl ? '見回り' : ''}/${workdetail_capture ? '捕獲' : ''}\n${workdetail_own ? '自身のわな' : ''}/${workdetail_help ? '捕獲手伝い' : ''}/${workdetail_mistake ? '錯誤捕獲' : ''}`; + + // 錯誤捕獲の組み立て + + const workdetail_trap_type = workdetail_mistake ? (document.getElementById('workdetail_trap_type') as HTMLInputElement).value : ''; + const workdetail_head_count = workdetail_mistake ? (document.getElementById('workdetail_head') as HTMLInputElement).value : ''; + const workdetail_response = workdetail_mistake ? (document.getElementById('workdetail_response') as HTMLInputElement).value : ''; + + const workdetail_deer = workdetail_mistake ? (document.getElementById('workdetail_deer') as HTMLInputElement).checked : false; + const workdetail_serow = workdetail_mistake ? (document.getElementById('workdetail_serow') as HTMLInputElement).checked : false; + const workdetail_boar = workdetail_mistake ? (document.getElementById('workdetail_boar') as HTMLInputElement).checked : false; + const workdetail_other = workdetail_mistake ? (document.getElementById('workdetail_other') as HTMLInputElement).checked : false; + + const workdetail_other_animal = workdetail_other ? (document.getElementById('workdetail_other_animal') as HTMLInputElement).value : ''; + + const workdetail_mistake_str = `${workdetail_trap_type}\n${workdetail_head_count}\n${workdetail_response}\n${workdetail_deer ? 'シカ' : ''}/${workdetail_serow ? 'ニホンジカ' : ''}/${workdetail_boar ? 'イノシシ' : ''}/${workdetail_other ? 'その他' : ''}\n${workdetail_other ? workdetail_other_animal : ''}`; + + const helper = (document.getElementById('report_b_helper') as HTMLInputElement).value; + + // とめさし道具の組み立て + const tool_elec = (document.getElementById('report_b_elec') as HTMLInputElement).checked; + const tool_gun = (document.getElementById('report_b_gun') as HTMLInputElement).checked; + const tool_other = (document.getElementById('report_b_other') as HTMLInputElement).checked; + + const tool_other_content = (document.getElementById('report_b_other_tool') as HTMLInputElement).value; + + const tool_str = `${tool_elec ? '電気とめさし器' : ''}/${tool_gun ? '銃' : ''}/${tool_other ? 'その他' : ''}\n${tool_other_content}`; + const user = props.featureInfo?.properties.入力者 != null ? props.featureInfo.properties.入力者 : currentUser?.userId; + const data: ReportFeature = { properties: { 入力者: user, @@ -44,12 +89,12 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp 作業報告: report, 備考: note, 画像ID: '', - 錯誤捕獲: 'TODO', - 止刺道具: 'TODO', - 捕獲補助: 'TODO', - 作業内容: 'TODO', - ワクチンNO: 'TODO', - 市町村字: 'TODO' + 錯誤捕獲: workdetail_mistake_str, + 止刺道具: tool_str, + 捕獲補助: helper, + 作業内容: workdetail, + ワクチンNO: vaccine_no, + 市町村字: city }, geometry: { type: 'Point', @@ -329,7 +374,12 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp required={true} error={errors.worktime} /> - <>ここに作業内容Window</> + <WorkDetailInput + id='workdetail' + workDefaultValue={featureValueOrUndefined('作業内容')} + mistakeDefaultValue={featureValueOrUndefined('錯誤捕獲')} + error={errors.workdetail} + /> <InfoInput title='作業結果・状況報告' subtitle='イノシシの痕跡、餌の摂食状況、わな設置場所の検討内容、わなの箇所数・基数、見回り活動で気づいたこと、餌で工夫したこと、改善点などを入力してください。' @@ -341,8 +391,12 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp error={errors.report} onChange={() => reportChanged()} /> - <>錯誤捕獲</> - <>ここに作業日報Bについて</> + <ReportBInput + id='report_b' + error={errors.report_b} + toolDefaultValue={featureValueOrUndefined('止刺道具')} + helperDefaultValue={featureValueOrUndefined('捕獲補助')} + /> <InfoInput title='備考' rows={3} diff --git a/src/components/templates/detailTemplate/index.tsx b/src/components/templates/detailTemplate/index.tsx index b3da705f..27f5f9aa 100644 --- a/src/components/templates/detailTemplate/index.tsx +++ b/src/components/templates/detailTemplate/index.tsx @@ -185,6 +185,8 @@ const DetailTemplate: React.FunctionComponent = () => { if (yesNoCheck) { router.push('/edit/location'); + } else if(isImageSkip) { + router.push('/edit/info'); } else { router.push('/edit/image'); } From 656e7fae1e31d55c0f194ac57c5b025ece536c8f Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Fri, 17 Jan 2025 03:35:19 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20=E4=BD=9C=E6=A5=AD=E6=97=A5?= =?UTF-8?q?=E5=A0=B1=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/molecules/infoDiv/index.tsx | 107 +++++++++ src/components/molecules/infoDiv/interface.ts | 4 +- .../organisms/featureViewer/index.tsx | 2 - .../organisms/reportInfoForm/index.tsx | 2 +- .../organisms/reportInfoView/index.tsx | 204 +++++++++++++++--- .../organisms/reportInfoView/interface.ts | 61 +++++- .../newDataForm/dataConfirmTemplate/index.tsx | 2 +- .../newDataForm/locationSelector/index.tsx | 2 +- 8 files changed, 344 insertions(+), 40 deletions(-) diff --git a/src/components/molecules/infoDiv/index.tsx b/src/components/molecules/infoDiv/index.tsx index 5ec1bdcd..bdf83f6b 100644 --- a/src/components/molecules/infoDiv/index.tsx +++ b/src/components/molecules/infoDiv/index.tsx @@ -3,6 +3,7 @@ import InfoText from '../../atomos/infoText'; import MiniMap from '../../atomos/miniMap'; import InfoTitle from '../../atomos/infoTitle'; import { InfoDivProps } from './interface'; +import { MistakeValue, ReportBServerValue, ToolValue, WorkValue } from '../../organisms/reportInfoView/interface'; const InfoDiv: React.FunctionComponent<InfoDivProps> = ({ type, data, unit, title }) => { const getTimeStr = (date_str: string): string => { @@ -73,6 +74,112 @@ const InfoDiv: React.FunctionComponent<InfoDivProps> = ({ type, data, unit, titl </> ); break; + case 'work': { + const [d1, d2] = data as unknown as [WorkValue, MistakeValue]; + + const vals = { + 'deer': 'ニホンジカ', + 'serow': 'カモシカ', + 'boar': 'ツキノワグマ', + 'other': `その他 (${d2.animal_other})` + }; + + dataDiv = ( + <div className="mb-4"> + <div className="text-xl"> + わな設置: {d1.trap.placed}基 / 撤去: {d1.trap.removed}基 {d1.crawl ? "/ 見回り" : ""} {d1.capture ? "/ 捕獲" : ""}<br /> + </div> + {d1.capture ? ( + <div className="text-lg"> + ({d1.capture_type.own ? "自身の罠で捕獲" : ""}{d1.capture_type.own && d1.capture_type.help ? "・" : ""}{d1.capture_type.help ? "捕獲手伝い" : ""}{(d1.capture_type.help && d1.capture_type.mistake) || (d1.capture_type.own && !d1.capture_type.help && d1.capture_type.mistake) ? "・" : ""}{d1.capture_type.mistake ? "錯誤捕獲" : ""}) + </div> + ) : <></>} + {d1.capture_type.mistake ? ( + <> + <InfoTitle>錯誤捕獲</InfoTitle> + <div className='inside-border box-border w-full'> + <InfoTitle>わなの種類</InfoTitle> + <div className="mb-4 text-xl"> + {d2.trap_type} + </div> + </div> + <div className='inside-border box-border w-full'> + <InfoTitle>獣種</InfoTitle> + <div className="mb-4 text-xl"> + {Object.keys(d2.animal_type).filter((key) => d2.animal_type[key as 'deer' | 'serow' | 'boar' | 'other']).map(d => vals[d as 'deer' | 'serow' | 'boar' | 'other']).join('・')} + </div> + </div> + <div className='inside-border box-border w-full'> + <InfoTitle>頭数</InfoTitle> + <div className="mb-4 text-xl"> + {d2.head_count} 頭 + </div> + </div> + <div className='inside-border box-border w-full'> + <InfoTitle>対応</InfoTitle> + <div className="mb-4 text-xl"> + {d2.response} + </div> + </div> + </> + ) : <></>} + </div> + ); + break; + } + case 'workB': { + const [d1, d2, d3] = data as unknown as [ReportBServerValue, string, ToolValue]; + const vals = { + 'elec': '電気とめさし器', + 'gun': '銃', + 'other': `その他 (${d3.other_tool})` + }; + + dataDiv = ( + <div className="mb-4"> + <div className='inside-border box-border w-full'> + <InfoTitle>捕獲者氏名</InfoTitle> + <div className="mb-4 text-xl"> + {d1.捕獲者} + </div> + </div> + <div className='inside-border box-border w-full'> + <InfoTitle>捕獲補助者氏名</InfoTitle> + <div className="mb-4 text-xl"> + {d2} + </div> + </div> + <div className='inside-border box-border w-full'> + <InfoTitle>とめさしの道具</InfoTitle> + <div className="mb-4 text-xl"> + {Object.keys(d3.tool).filter((key) => d3.tool[key as 'elec' | 'gun' | 'other']).map(d => vals[d as 'elec' | 'gun' | 'other']).join('・')} + </div> + </div> + <InfoTitle>捕獲情報</InfoTitle> + {Object.keys(d1.捕獲数).filter(key => d1.捕獲数[key as keyof typeof d1.捕獲数].捕獲数 != 0).map((key) => ( + <div className='inside-border box-border w-full' key={key}> + <InfoTitle>{key.substring(0, 2)}({key.substring(2, 4)})</InfoTitle> + <div className="mb-4 text-xl"> + {d1.捕獲数[key as keyof typeof d1.捕獲数].捕獲数} 頭 / {d1.捕獲数[key as keyof typeof d1.捕獲数].体長} cm / {d1.捕獲数[key as keyof typeof d1.捕獲数].処分方法.join('・')} + </div> + </div> + ))} + <div className='inside-border box-border w-full'> + <InfoTitle>遠沈管番号</InfoTitle> + <div className="mb-4 text-xl"> + {d1.遠沈管番号.join('、')} + </div> + </div> + <div className='inside-border box-border w-full'> + <InfoTitle>わなの種類</InfoTitle> + <div className="mb-4 text-xl"> + {d1.わなの種類.join('・')} + </div> + </div> + </div> + ); + break; + } case 'text': default: dataDiv = <InfoText>{data}</InfoText>; diff --git a/src/components/molecules/infoDiv/interface.ts b/src/components/molecules/infoDiv/interface.ts index d5def081..a7ba7abe 100644 --- a/src/components/molecules/infoDiv/interface.ts +++ b/src/components/molecules/infoDiv/interface.ts @@ -1,6 +1,6 @@ export interface InfoDivProps { - type?: 'text' | 'date' | 'number' | 'location' | 'images' | 'gray' | 'period'; - data?: string | Record<string, unknown>; + type?: 'text' | 'date' | 'number' | 'location' | 'images' | 'gray' | 'period' | 'work' | 'workB'; + data?: string | Record<string, unknown> | unknown[]; unit?: string; title: string; } diff --git a/src/components/organisms/featureViewer/index.tsx b/src/components/organisms/featureViewer/index.tsx index 0f655cb7..b680d00c 100644 --- a/src/components/organisms/featureViewer/index.tsx +++ b/src/components/organisms/featureViewer/index.tsx @@ -71,8 +71,6 @@ const FeatureViewer: React.FunctionComponent<FeatureViewerProps> = ({ infoDiv = ( <ReportInfoView detail={featureInfo as ReportFeature} - imageIDs={imageIDs} - objectURLs={objectURLs} confirmMode={confirm} /> ); diff --git a/src/components/organisms/reportInfoForm/index.tsx b/src/components/organisms/reportInfoForm/index.tsx index 7463acd7..726c078d 100644 --- a/src/components/organisms/reportInfoForm/index.tsx +++ b/src/components/organisms/reportInfoForm/index.tsx @@ -60,7 +60,7 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp const workdetail_other_animal = workdetail_other ? (document.getElementById('workdetail_other_animal') as HTMLInputElement).value : ''; - const workdetail_mistake_str = `${workdetail_trap_type}\n${workdetail_head_count}\n${workdetail_response}\n${workdetail_deer ? 'シカ' : ''}/${workdetail_serow ? 'ニホンジカ' : ''}/${workdetail_boar ? 'イノシシ' : ''}/${workdetail_other ? 'その他' : ''}\n${workdetail_other ? workdetail_other_animal : ''}`; + const workdetail_mistake_str = `${workdetail_trap_type}\n${workdetail_head_count}\n${workdetail_response}\n${workdetail_deer ? 'ニホンジカ' : ''}/${workdetail_serow ? 'カモシカ' : ''}/${workdetail_boar ? 'ツキノワグマ' : ''}/${workdetail_other ? 'その他' : ''}\n${workdetail_other ? workdetail_other_animal : ''}`; const helper = (document.getElementById('report_b_helper') as HTMLInputElement).value; diff --git a/src/components/organisms/reportInfoView/index.tsx b/src/components/organisms/reportInfoView/index.tsx index f79decca..675e02f7 100644 --- a/src/components/organisms/reportInfoView/index.tsx +++ b/src/components/organisms/reportInfoView/index.tsx @@ -1,41 +1,183 @@ +import { useEffect, useMemo, useState } from 'react'; import InfoDiv from '../../molecules/infoDiv'; -import { ReportInfoViewProps } from './interface'; +import { ReportBBodyValue, ReportInfoViewProps } from './interface'; +import { SERVER_URI } from '../../../utils/constants'; +import { getAccessToken } from '../../../utils/currentUser'; +import { useRouter } from 'next/router'; +import { alert } from '../../../utils/modal'; const ReportInfoView: React.FunctionComponent<ReportInfoViewProps> = ({ detail, - objectURLs, - imageIDs, confirmMode, }) => { + const router = useRouter(); + + const [reportBValue, setReportBValue] = useState<ReportBBodyValue | undefined>(undefined); + + const parsedWorkValue = useMemo(() => { + const data = { + trap: { + placed: 0, + removed: 0 + }, + capture: false, + crawl: false, + capture_type: { + own: false, + help: false, + mistake: false + } + }; + + if (!detail.properties.作業内容) { + return data; + } + + + // データ構造は下記の通り (スラッシュは未チェック時も挿入される) + // (わな設置)/(撤去)/(見回り(チェック時))/(捕獲(チェック時)) + // 自身のわな/捕獲手伝い/錯誤捕獲 + + const lines = detail.properties.作業内容.split('\n'); + const trap = (lines[0] || '').split('/'); + data.trap.placed = isNaN(parseInt(trap[0])) ? 0 : parseInt(trap[0]); + data.trap.removed = isNaN(parseInt(trap[1])) ? 0 : parseInt(trap[1]); + if (trap[2] === '見回り') { + data.crawl = true; + } + if (trap[3] === '捕獲') { + data.capture = true; + } + + const capture = (lines[1] || '').split('/'); + data.capture_type.own = capture[0] === '自身のわな'; + data.capture_type.help = capture[1] === '捕獲手伝い'; + data.capture_type.mistake = capture[2] === '錯誤捕獲'; + + return data; + }, [detail.properties.作業内容]); + + const parsedMistakeValue = useMemo(() => { + const data = { + trap_type: '', + head_count: 0, + response: '', + animal_type: { + deer: false, + serow: false, + boar: false, + other: false + }, + animal_other: '' + }; + + if (!detail.properties.錯誤捕獲) { + return data; + } + + // データ構造は下記の通り + // わなの種類 + // 頭数 + // 対応 + // (ニホンジカ(チェック時))/(カモシカ(チェック時))/(ツキノワグマ(チェック時))/(その他(チェック時)) + // その他の場合の獣種 + + const lines = detail.properties.錯誤捕獲.split('\n'); + data.trap_type = (lines[0] || ''); + data.head_count = isNaN(parseInt(lines[1])) ? 0 : parseInt(lines[1]); + data.response = (lines[2] || ''); + + const animal = (lines[3] || '').split('/'); + data.animal_type.deer = animal[0] === 'ニホンジカ'; + data.animal_type.serow = animal[1] === 'カモシカ'; + data.animal_type.boar = animal[2] === 'ツキノワグマ'; + data.animal_type.other = animal[3] === 'その他'; + + data.animal_other = lines[4]; + + return data; + }, [detail.properties.錯誤捕獲]); + + const parsedToolValue = useMemo(() => { + // データ構造は下記の通り + // (電気とめさし器(チェック時))/(銃(チェック時))/(その他(チェック時)) + // その他の場合の内容 + const data = { + tool: { + elec: false, + gun: false, + other: false + }, + other_tool: '' + }; + + if (!detail.properties.止刺道具) { + return data; + } + + const lines = detail.properties.止刺道具.split('\n'); + const tool = (lines[0] || '').split('/'); + data.tool.elec = tool[0] === '電気とめさし器'; + data.tool.gun = tool[1] === '銃'; + data.tool.other = tool[2] === 'その他'; + + data.other_tool = lines[1]; + + return data; + }, [detail.properties.止刺道具]); + + useEffect(() => { + const asyncTask = async () => { + const response = await fetch(`${SERVER_URI}/Features/ReportCalc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Token': getAccessToken(), + }, + body: JSON.stringify({ + date: detail.properties.作業開始時, + user: detail.properties.入力者 + }), + }); + + if (!response.ok) { + alert('データの取得に失敗しました。'); + router.push('/map'); + return; + } + + const json = await response.json(); + setReportBValue(json); + }; + + asyncTask(); + }, [confirmMode]); + return ( - <div className='box-border w-full'> - <InfoDiv - title='場所' - type='location' - data={{ lat: detail.geometry.coordinates[1], lng: detail.geometry.coordinates[0] }} - /> - <InfoDiv - title='画像' - type='images' - data={{ - type: 'trap', - objectURLs: objectURLs, - confirmMode: confirmMode, - imageIDs: - imageIDs != null ? imageIDs : detail.properties.画像ID.split(',').filter((e) => e), - }} - /> - <InfoDiv title='地域(農林事務所単位)' type='text' data={detail.properties.地域} /> - <InfoDiv title='所属支部名' type='text' data={detail.properties.所属支部名} /> - <InfoDiv title='氏名' type='text' data={detail.properties.氏名} /> - <InfoDiv - title='作業時間' - type='period' - data={{ start: detail.properties.作業開始時, end: detail.properties.作業終了時 }} - /> - <InfoDiv title='作業報告・状況報告' type='text' data={detail.properties.作業報告} /> - <InfoDiv title='備考' type='text' data={detail.properties.備考} /> - </div> + reportBValue != undefined ? ( + <div className='box-border w-full'> + <InfoDiv + title='場所' + type='location' + data={{ lat: detail.geometry.coordinates[1], lng: detail.geometry.coordinates[0] }} + /> + <InfoDiv title='所属支部名' type='text' data={detail.properties.所属支部名} /> + <InfoDiv title='氏名' type='text' data={detail.properties.氏名} /> + <InfoDiv title="わなの場所(市町村・字)" data={detail.properties.市町村字} /> + <InfoDiv title="わなの場所(ワクチンメッシュ番号)" data={detail.properties.ワクチンNO} /> + <InfoDiv + title='作業時間' + type='period' + data={{ start: detail.properties.作業開始時, end: detail.properties.作業終了時 }} + /> + <InfoDiv title='作業内容' type='work' data={[parsedWorkValue, parsedMistakeValue]} /> + <InfoDiv title='作業報告・状況報告' type='text' data={detail.properties.作業報告} /> + <InfoDiv title='作業報告B' type='workB' data={[reportBValue, detail.properties.捕獲補助, parsedToolValue]} /> + <InfoDiv title='備考' type='text' data={detail.properties.備考} /> + </div> + ) : ( + <div className='pt-6 text-center text-3xl font-bold'>読み込み中...</div> + ) ); }; diff --git a/src/components/organisms/reportInfoView/interface.ts b/src/components/organisms/reportInfoView/interface.ts index cf07b581..969686f5 100644 --- a/src/components/organisms/reportInfoView/interface.ts +++ b/src/components/organisms/reportInfoView/interface.ts @@ -2,7 +2,64 @@ import { ReportFeature } from '../../../types/features'; export interface ReportInfoViewProps { detail: ReportFeature; - objectURLs?: string[]; confirmMode?: boolean; - imageIDs?: string[]; } + +export interface ReportBBodyValue { + 捕獲数: number; + 体長: number; + 処分方法: string[]; +} + +export interface ReportBServerValue { + 捕獲者: string; + 捕獲数: { + 成獣オス: ReportBBodyValue; + 成獣メス: ReportBBodyValue; + 成獣不明: ReportBBodyValue; + 幼獣オス: ReportBBodyValue; + 幼獣メス: ReportBBodyValue; + 幼獣不明: ReportBBodyValue; + } + 成獣: ReportBBodyValue; + 幼獣オス: ReportBBodyValue; + 幼獣メス: ReportBBodyValue; + 遠沈管番号: string[]; + わなの種類: string[]; +} + +export interface WorkValue { + trap: { + placed: number, + removed: number + }, + capture: boolean, + crawl: boolean, + capture_type: { + own: boolean, + help: boolean, + mistake: boolean, + } +} + +export interface MistakeValue { + trap_type: string, + head_count: number, + response: string, + animal_type: { + deer: boolean, + serow: boolean, + boar: boolean, + other: boolean, + }, + animal_other: string +} + +export interface ToolValue { + tool: { + elec: boolean, + gun: boolean, + other: boolean, + }, + other_tool: string +} \ No newline at end of file diff --git a/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx b/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx index 5d92317f..f57acd3a 100644 --- a/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx +++ b/src/components/templates/newDataForm/dataConfirmTemplate/index.tsx @@ -114,7 +114,7 @@ const DataConfirmTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEditing // 削除される画像リストの取得 const origImg = paramParser.currentData.editData?.curImg.other.concat(paramParser.currentData.editData?.curImg.teeth) || []; - const delImg = origImg.filter(e => !serverImages?.includes(e)); + const delImg = origImg.filter(e => !serverImages?.includes(e)).filter(e => e); setIsLoading(true); diff --git a/src/components/templates/newDataForm/locationSelector/index.tsx b/src/components/templates/newDataForm/locationSelector/index.tsx index 55d67aed..71b4bb6a 100644 --- a/src/components/templates/newDataForm/locationSelector/index.tsx +++ b/src/components/templates/newDataForm/locationSelector/index.tsx @@ -41,7 +41,7 @@ const LocationSelectorTemplate: React.FC<InputFormTemplateCommonProps> = ({ isEd } const coordinates = paramParser.currentData.inputData.gisData?.geometry?.coordinates ?? [NaN, NaN]; - const isLocationExists = coordinates.filter((e) => !isNaN(e)).length == 2; + const isLocationExists = coordinates.filter((e) => e != null && !isNaN(e)).length == 2; if (isLocationExists) { // ポイント情報が既に存在する場合はその位置を初期位置とする const coordinates = paramParser.currentData.inputData.gisData?.geometry?.coordinates ?? [35.39135, 136.722418]; From beca53d02f9b5b8c1b50ea1718852ef3945ba5f3 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Fri, 17 Jan 2025 03:55:03 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20=E4=B8=80=E8=A6=A7=E8=A1=A8?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=81=AB=E8=BB=BD=E5=BE=AE=E3=81=AA=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/organisms/boarTable/index.tsx | 55 +++++++++---------- .../organisms/reportTable/index.tsx | 49 ++++++++--------- src/components/organisms/searchForm/index.tsx | 2 +- src/components/organisms/trapTable/index.tsx | 49 ++++++++--------- .../organisms/vaccineTable/index.tsx | 49 ++++++++--------- .../templates/detailTemplate/index.tsx | 2 +- 6 files changed, 97 insertions(+), 109 deletions(-) diff --git a/src/components/organisms/boarTable/index.tsx b/src/components/organisms/boarTable/index.tsx index 36efd98a..019adb5a 100644 --- a/src/components/organisms/boarTable/index.tsx +++ b/src/components/organisms/boarTable/index.tsx @@ -16,9 +16,11 @@ import { alert, yesNo } from '../../../utils/modal'; import { sortFeatures } from '../../../utils/sort'; import RoundButton from '../../atomos/roundButton'; import { BoarTableProps } from './interface'; +import { useFormDataParser } from '../../../utils/form-data'; const BoarTable: React.FunctionComponent<BoarTableProps> = (p) => { const router = useRouter(); + const paramParser = useFormDataParser(); const { currentUser } = useCurrentUser(); const [sortKey, setSortKey] = useState('ID$'); const [isDesc, setDesc] = useState(false); @@ -75,36 +77,31 @@ const BoarTable: React.FunctionComponent<BoarTableProps> = (p) => { feature: FeatureBase, ) => { if (!id && !version) return; - + const yesNoCheck = await yesNo('位置情報の編集を行いますか?\n\n※ 検体到着予定日以降に修正する場合は、下記にご連絡ください。\nTel. 058-272-8096 (平日8:30~12:00、13:00~17:15)'); + paramParser.updateData({ + dataType: 'boar', + isLocationSkipped: !yesNoCheck, + isImageSkipped: false, + inputData: { + gisData: feature, + }, + editData: { + id: id as string, + type: 'いのしし捕獲地点', + type_srv: `boar-${version}`, + version: `${version}`, + curImg: { + teeth: ((feature.properties as Record<string, string>)['歯列写真ID'] || '').split(','), + other: ((feature.properties as Record<string, string>)[`boar-${version}` === 'boar-2' ? '写真ID' : '画像ID'] || '').split(','), + } + } + }); + if (yesNoCheck) { - router.push( - { - pathname: '/edit-old/location', - query: { - id: id, - type: 'いのしし捕獲地点', - type_srv: `boar-${version}`, - version: version, - detail: JSON.stringify(feature), - }, - }, - '/edit-old/location', - ); + router.push('/edit/location'); } else { - router.push( - { - pathname: '/edit-old/image', - query: { - id: id, - type: 'いのしし捕獲地点', - type_srv: `boar-${version}`, - version: version, - detail: JSON.stringify(feature), - }, - }, - '/edit-old/image', - ); + router.push('/edit/image'); } }; @@ -132,7 +129,7 @@ const BoarTable: React.FunctionComponent<BoarTableProps> = (p) => { if (type === 'boar-2') { const sid = (feature.properties as Record<string, unknown>)['歯列写真ID'] as string; if (sid != null && sid !== '') { - imageIds.push(sid); + sid.split(',').filter(e=>e).forEach((e) => imageIds.push(e)); } } @@ -324,7 +321,7 @@ const BoarTable: React.FunctionComponent<BoarTableProps> = (p) => { </tr> {features.map((f, i) => { const props = f.properties as BoarFeaturePropsV2; - const imageList = [props.歯列写真ID, ...props.写真ID.split(',')].filter((e) => e); + const imageList = [...props.歯列写真ID.split(','), ...props.写真ID.split(',')].filter((e) => e); return props.捕獲いのしし情報.map((_, index, arr) => { const d = arr[index].properties; return ( diff --git a/src/components/organisms/reportTable/index.tsx b/src/components/organisms/reportTable/index.tsx index f0cfc619..e1496c33 100644 --- a/src/components/organisms/reportTable/index.tsx +++ b/src/components/organisms/reportTable/index.tsx @@ -10,9 +10,11 @@ import { alert, yesNo } from '../../../utils/modal'; import { sortFeatures } from '../../../utils/sort'; import RoundButton from '../../atomos/roundButton'; import { ReportTableProps } from './interface'; +import { useFormDataParser } from '../../../utils/form-data'; const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { const router = useRouter(); + const paramParser = useFormDataParser(); const { currentUser } = useCurrentUser(); const [sortKey, setSortKey] = useState('ID$'); const [isDesc, setDesc] = useState(false); @@ -64,34 +66,29 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { if (!id) return; const yesNoCheck = await yesNo('位置情報の編集を行いますか?'); + paramParser.updateData({ + dataType: 'report', + isLocationSkipped: !yesNoCheck, + isImageSkipped: true, + inputData: { + gisData: feature, + }, + editData: { + id: id as string, + type: '作業日報', + type_srv: `report`, + version: `1`, + curImg: { + teeth: ((feature.properties as Record<string, string>)['歯列写真ID'] || '').split(','), + other: ((feature.properties as Record<string, string>)['画像ID'] || '').split(','), + } + } + }); + if (yesNoCheck) { - router.push( - { - pathname: '/edit-old/location', - query: { - id: id, - type: '作業日報', - type_srv: `report`, - detail: JSON.stringify(feature), - version: 1, - }, - }, - '/edit-old/location', - ); + router.push('/edit/location'); } else { - router.push( - { - pathname: '/edit-old/image', - query: { - id: id, - type: '作業日報', - type_srv: `report`, - detail: JSON.stringify(feature), - version: 1, - }, - }, - '/edit-old/image', - ); + router.push('/edit/info'); } }; diff --git a/src/components/organisms/searchForm/index.tsx b/src/components/organisms/searchForm/index.tsx index 9d6f85e1..bcc4bca7 100644 --- a/src/components/organisms/searchForm/index.tsx +++ b/src/components/organisms/searchForm/index.tsx @@ -36,7 +36,7 @@ const SearchForm: React.FunctionComponent<SearchFormProps> = ({ onClick }) => { list.push('ワクチン散布地点'); if ( currentUser.userDepartment === 'T' || - currentUser.userDepartment === 'R' || + currentUser.userDepartment === 'D' || currentUser.userDepartment === 'K' ) list.push('作業日報'); diff --git a/src/components/organisms/trapTable/index.tsx b/src/components/organisms/trapTable/index.tsx index c17f8f29..3f97b238 100644 --- a/src/components/organisms/trapTable/index.tsx +++ b/src/components/organisms/trapTable/index.tsx @@ -10,9 +10,11 @@ import { alert, yesNo } from '../../../utils/modal'; import { sortFeatures } from '../../../utils/sort'; import RoundButton from '../../atomos/roundButton'; import { TrapTableProps } from './interface'; +import { useFormDataParser } from '../../../utils/form-data'; const TrapTable: React.FunctionComponent<TrapTableProps> = (p) => { const router = useRouter(); + const paramParser = useFormDataParser(); const { currentUser } = useCurrentUser(); const [sortKey, setSortKey] = useState('ID$'); const [isDesc, setDesc] = useState(false); @@ -64,34 +66,29 @@ const TrapTable: React.FunctionComponent<TrapTableProps> = (p) => { if (!id) return; const yesNoCheck = await yesNo('位置情報の編集を行いますか?'); + paramParser.updateData({ + dataType: 'trap', + isLocationSkipped: !yesNoCheck, + isImageSkipped: false, + inputData: { + gisData: feature, + }, + editData: { + id: id as string, + type: 'わな設置地点', + type_srv: `trap`, + version: `1`, + curImg: { + teeth: ((feature.properties as Record<string, string>)['歯列写真ID'] || '').split(','), + other: ((feature.properties as Record<string, string>)['画像ID'] || '').split(','), + } + } + }); + if (yesNoCheck) { - router.push( - { - pathname: '/edit-old/location', - query: { - id: id, - type: 'わな設置地点', - type_srv: `trap`, - detail: JSON.stringify(feature), - version: 1, - }, - }, - '/edit-old/location', - ); + router.push('/edit/location'); } else { - router.push( - { - pathname: '/edit-old/image', - query: { - id: id, - type: 'わな設置地点', - type_srv: `trap`, - detail: JSON.stringify(feature), - version: 1, - }, - }, - '/edit-old/image', - ); + router.push('/edit/image'); } }; diff --git a/src/components/organisms/vaccineTable/index.tsx b/src/components/organisms/vaccineTable/index.tsx index 8083e0e5..018fc855 100644 --- a/src/components/organisms/vaccineTable/index.tsx +++ b/src/components/organisms/vaccineTable/index.tsx @@ -10,9 +10,11 @@ import { alert, yesNo } from '../../../utils/modal'; import { sortFeatures } from '../../../utils/sort'; import RoundButton from '../../atomos/roundButton'; import { VaccineTableProps } from './interface'; +import { useFormDataParser } from '../../../utils/form-data'; const VaccineTable: React.FunctionComponent<VaccineTableProps> = (p) => { const router = useRouter(); + const paramParser = useFormDataParser(); const { currentUser } = useCurrentUser(); const [sortKey, setSortKey] = useState('ID$'); const [isDesc, setDesc] = useState(false); @@ -64,34 +66,29 @@ const VaccineTable: React.FunctionComponent<VaccineTableProps> = (p) => { if (!id) return; const yesNoCheck = await yesNo('位置情報の編集を行いますか?'); + paramParser.updateData({ + dataType: 'boar', + isLocationSkipped: !yesNoCheck, + isImageSkipped: false, + inputData: { + gisData: feature, + }, + editData: { + id: id as string, + type: 'ワクチン散布地点', + type_srv: `vaccine`, + version: `1`, + curImg: { + teeth: ((feature.properties as Record<string, string>)['歯列写真ID'] || '').split(','), + other: ((feature.properties as Record<string, string>)['画像ID'] || '').split(','), + } + } + }); + if (yesNoCheck) { - router.push( - { - pathname: '/edit-old/location', - query: { - id: id, - type: 'ワクチン散布地点', - type_srv: `vaccine`, - detail: JSON.stringify(feature), - version: 1, - }, - }, - '/edit-old/location', - ); + router.push('/edit/location'); } else { - router.push( - { - pathname: '/edit-old/image', - query: { - id: id, - type: 'ワクチン散布地点', - type_srv: `vaccine`, - detail: JSON.stringify(feature), - version: 1, - }, - }, - '/edit-old/image', - ); + router.push('/edit/image'); } }; diff --git a/src/components/templates/detailTemplate/index.tsx b/src/components/templates/detailTemplate/index.tsx index 27f5f9aa..acd3aace 100644 --- a/src/components/templates/detailTemplate/index.tsx +++ b/src/components/templates/detailTemplate/index.tsx @@ -85,7 +85,7 @@ const DetailTemplate: React.FunctionComponent = () => { if (featureType === 'boar-2') { const sid = (featureInfo.properties as Record<string, unknown>)['歯列写真ID'] as string; if (sid != null && sid !== '') { - imageIds.push(sid); + sid.split(',').filter(e=>e).forEach((e) => imageIds.push(e)); } } From 5c247038a60de1a16e42705cafde06df3bde6a74 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Sun, 19 Jan 2025 22:15:34 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20=E4=BD=9C=E6=A5=AD=E6=97=A5?= =?UTF-8?q?=E5=A0=B1=E3=82=92=E5=87=BA=E5=8A=9B=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organisms/reportTable/index.tsx | 120 ++++++++++-------- .../organisms/searchResult/index.tsx | 16 ++- 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/src/components/organisms/reportTable/index.tsx b/src/components/organisms/reportTable/index.tsx index e1496c33..0accd59f 100644 --- a/src/components/organisms/reportTable/index.tsx +++ b/src/components/organisms/reportTable/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @next/next/no-img-element */ import { useRouter } from 'next/router'; -import { SyntheticEvent, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useCurrentUser } from '../../../hooks/useCurrentUser'; import { ReportProps, FeatureBase, ReportFeature } from '../../../types/features'; import { SERVER_URI } from '../../../utils/constants'; @@ -21,6 +21,7 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { const [features, setFeatures] = useState<FeatureBase[]>([]); const [editable, setEditable] = useState(false); const [deletedFeatures, setDeletedFeatures] = useState<string[]>([]); + const [isDownloading, setDownloading] = useState(false); const sortableClass = (key: string) => { if (key == sortKey) { @@ -92,6 +93,48 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { } }; + const onClickPdfDownload = useCallback(async (id: string, feature: ReportFeature) => { + setDownloading(true); + const res = await fetch(SERVER_URI + '/Report/Export', { + method: 'POST', + body: JSON.stringify({ id }), + headers: { + Accept: 'application/pdf', + 'X-Access-Token': getAccessToken(), + }, + }); + + if (res.status === 200) { + const blob = await res.blob(); + const anchor = document.createElement('a'); + + const dtStr = feature.properties.作業開始時; + const dt = new Date(dtStr); + const yyyy = ('0000' + dt.getFullYear()).slice(-4); + const mm = ('00' + (dt.getMonth() + 1)).slice(-2); + const dd = ('00' + dt.getDate()).slice(-2); + + const name = '作業日報 - ' + feature.properties.入力者 + ' (' + yyyy + '-' + mm + '-' + dd + ').pdf'; + // IE対応 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (window.navigator.msSaveBlob) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + window.navigator.msSaveBlob(blob, name); + setDownloading(false); + return; + } + anchor.download = name; + anchor.href = window.URL.createObjectURL(blob); + anchor.click(); + setDownloading(false); + } else { + const json = await res.json(); + await alert(json.error); + } + }, []); + const onClickDelete = async (id: string | undefined, feature: FeatureBase) => { if(!await confirm(`ID: ${id}の情報を削除しますか?`)) return; @@ -179,6 +222,7 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { <table className='block border-collapse whitespace-pre'> <tbody className='table'> <tr> + <th className={'border border-b-2 border-solid border-border p-1'}></th> <th className={'border border-b-2 border-solid border-border p-1'}></th> <th className={'border border-b-2 border-solid border-border p-1 ' + sortableClass('ID$')} @@ -220,6 +264,21 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { > 氏名 </th> + <th className={'border border-b-2 border-solid border-border p-1 '}> + 捕獲補助者 + </th> + <th + className={ + 'border border-b-2 border-solid border-border p-1 ' + sortableClass('市町村字') + } + onClick={() => sort('市町村字')} + >市町村・字</th> + <th + className={ + 'border border-b-2 border-solid border-border p-1 ' + sortableClass('ワクチンNO') + } + onClick={() => sort('ワクチンNO')} + >ワクチンメッシュ番号</th> <th className={ 'border border-b-2 border-solid border-border p-1 ' + sortableClass('作業開始時') @@ -246,11 +305,9 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { 状況報告 </th> <th className={'border border-b-2 border-solid border-border p-1 '}>備考</th> - <th className={'border border-b-2 border-solid border-border p-1'}>写真</th> </tr> {features.map((f, i) => { const props = f.properties as ReportProps; - const imageList = props.画像ID.split(',').filter((e) => e); return ( <tr key={'data-' + (i + 1) + '-trap'}> <td className='border border-solid border-border p-1 text-right'> @@ -267,11 +324,18 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { </RoundButton> </div> </td> + <td className='border border-solid border-border p-1'> + <RoundButton color='excel' onClick={() => onClickPdfDownload(`${props.ID$}`, f as ReportFeature)} disabled={isDownloading}> + ダウンロード + </RoundButton></td> <td className='border border-solid border-border p-1 text-right'>{props.ID$}</td> <td className='border border-solid border-border p-1'>{props.入力者}</td> <td className='border border-solid border-border p-1'>{props.地域}</td> <td className='border border-solid border-border p-1'>{props.所属支部名}</td> <td className='border border-solid border-border p-1'>{props.氏名}</td> + <td className='border border-solid border-border p-1'>{props.捕獲補助}</td> + <td className='border border-solid border-border p-1'>{props.市町村字}</td> + <td className='border border-solid border-border p-1'>{props.ワクチンNO}</td> <td className='border border-solid border-border p-1 text-right'> {props.作業開始時.split(' ')[0]} <br /> @@ -284,56 +348,6 @@ const ReportTable: React.FunctionComponent<ReportTableProps> = (p) => { </td> <td className='border border-solid border-border p-1'>{props.作業報告}</td> <td className='border border-solid border-border p-1'>{props.備考}</td> - - <td className='border border-solid border-border p-1'> - <div className='flex w-[300px] flex-wrap'> - {imageList.length == 0 ? ( - <div>画像なし</div> - ) : ( - imageList - .filter((e) => e) - .map((v, img_i) => { - const url = `${SERVER_URI}/Image/GetImage?id=${v}&token=${getAccessToken()}`; - - // 200px x 200pxでエリアを確保しておき、ロード後に長辺200pxになるようにリサイズする - const onLoaded = (e: SyntheticEvent<HTMLImageElement>) => { - const elem = e.target as HTMLImageElement; - - const calcShort = (long: number, short: number) => { - return short * (200.0 / long); - }; - - const w = - elem.naturalWidth > elem.naturalHeight - ? 200 - : calcShort(elem.naturalHeight, elem.naturalWidth); - const h = - elem.naturalWidth > elem.naturalHeight - ? calcShort(elem.naturalWidth, elem.naturalHeight) - : 200; - elem.setAttribute('style', `width: ${w}px; height: ${h}px;`); - }; - - return ( - <a - href={url} - rel='noopener noreferrer' - target='_blank' - key={'Image_' + (img_i + 1) + '_' + props.ID$} - > - <img - src={url} - alt={'Image ' + (img_i + 1) + ' of ID ' + props.ID$} - className='m-[5px] max-w-none' - style={{ width: '200px', height: '200px' }} - onLoad={onLoaded} - /> - </a> - ); - }) - )} - </div> - </td> </tr> ); })} diff --git a/src/components/organisms/searchResult/index.tsx b/src/components/organisms/searchResult/index.tsx index 7b34d2d5..77f5a5f0 100644 --- a/src/components/organisms/searchResult/index.tsx +++ b/src/components/organisms/searchResult/index.tsx @@ -26,6 +26,10 @@ const SearchResult: React.FunctionComponent<SearchResultProps> = ({ searchInfo, }, []); const onClickDownload = async () => { + if (searchInfo.get('type') == '作業日報') { + return; + } + setDownloading(true); const res = await fetch(SERVER_URI + '/List/Export', { method: 'POST', @@ -69,11 +73,13 @@ const SearchResult: React.FunctionComponent<SearchResultProps> = ({ searchInfo, <div className='mr-4 inline-block w-full'> <div className='relative mb-3 h-auto text-2xl font-bold'> 検索結果 - <div className='ml-5 inline-block w-52'> - <RoundButton color='excel' onClick={onClickDownload} disabled={isDownloading}> - {isDownloading ? 'ダウンロード中...' : 'ダウンロード'} - </RoundButton> - </div> + {searchInfo.get('type') == '作業日報' ? <></> : ( + <div className='ml-5 inline-block w-52'> + <RoundButton color='excel' onClick={onClickDownload} disabled={isDownloading}> + {isDownloading ? 'ダウンロード中...' : 'ダウンロード'} + </RoundButton> + </div>) + } </div> <div> {searchInfo.get('type') == 'いのしし捕獲地点' ? ( From 2aab4df888b3a16181c02ab19a655577d2a365d1 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Sun, 19 Jan 2025 23:20:17 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20=E3=81=8A=E7=9F=A5=E3=82=89?= =?UTF-8?q?=E3=81=9B=E3=81=AE=E7=B7=A8=E9=9B=86=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/components/atomos/TextInput/index.tsx | 1 + src/components/atomos/TextInput/interface.ts | 1 + src/components/atomos/textAreaInput/index.tsx | 1 + .../atomos/textAreaInput/interface.ts | 1 + .../templates/indexTemplate/index.tsx | 2 +- .../templates/loginTemplate/index.tsx | 33 ++- .../templates/loginTemplate/interface.ts | 5 + .../noticeSettingsTemplate/index.tsx | 206 ++++++++++++++++++ .../templates/settingsTemplate/index.tsx | 10 + src/pages/settings/notice.tsx | 14 ++ 11 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 src/components/templates/noticeSettingsTemplate/index.tsx create mode 100644 src/pages/settings/notice.tsx diff --git a/README.md b/README.md index f606f9db..a10dbff6 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,4 @@ $ yarn start 1. https://boarmap-dev.gifu-nct.ac.jp/ (developブランチ) ## 著作権表記 -Copyright (c) 2019-2022 National Institute of Technology, Gifu College GIS Team +Copyright (c) 2019-2025 National Institute of Technology, Gifu College GIS Team diff --git a/src/components/atomos/TextInput/index.tsx b/src/components/atomos/TextInput/index.tsx index 61e5d821..54f6192d 100644 --- a/src/components/atomos/TextInput/index.tsx +++ b/src/components/atomos/TextInput/index.tsx @@ -19,6 +19,7 @@ const TextInput: React.FunctionComponent<TextInputProps> = (props) => { 'box-border w-full rounded-lg border-2 border-solid p-2 text-lg ' + (props.isError ? 'border-danger bg-input-error-bg' : 'border-border bg-input-bg') } + value={props.value} /> </div> ); diff --git a/src/components/atomos/TextInput/interface.ts b/src/components/atomos/TextInput/interface.ts index 56954a8d..1ed58c09 100644 --- a/src/components/atomos/TextInput/interface.ts +++ b/src/components/atomos/TextInput/interface.ts @@ -11,4 +11,5 @@ export interface TextInputProps { disabled?: boolean; required?: boolean; isError?: boolean; + value?: string | number; } diff --git a/src/components/atomos/textAreaInput/index.tsx b/src/components/atomos/textAreaInput/index.tsx index e3c00809..0760895c 100644 --- a/src/components/atomos/textAreaInput/index.tsx +++ b/src/components/atomos/textAreaInput/index.tsx @@ -16,6 +16,7 @@ const TextAreaInput: React.FunctionComponent<TextAreaInputProps> = (props) => { placeholder={props.placeholder} defaultValue={props.defaultValue} onChange={props.onChange} + value={props.value} > {props.children} </textarea> diff --git a/src/components/atomos/textAreaInput/interface.ts b/src/components/atomos/textAreaInput/interface.ts index 9003f2b0..fcf03b5b 100644 --- a/src/components/atomos/textAreaInput/interface.ts +++ b/src/components/atomos/textAreaInput/interface.ts @@ -8,4 +8,5 @@ export interface TextAreaInputProps { onChange?(): void; required?: boolean; error?: boolean; + value?: string; } diff --git a/src/components/templates/indexTemplate/index.tsx b/src/components/templates/indexTemplate/index.tsx index fbc6c72e..1df2590d 100644 --- a/src/components/templates/indexTemplate/index.tsx +++ b/src/components/templates/indexTemplate/index.tsx @@ -42,7 +42,7 @@ const IndexTemplate: React.FunctionComponent = () => { </div> </div> <div className='pt-8 text-center'> - © 2019-2022 National Institute of Technology, Gifu College GIS Team + © 2019-2025 National Institute of Technology, Gifu College GIS Team </div> </div> </div> diff --git a/src/components/templates/loginTemplate/index.tsx b/src/components/templates/loginTemplate/index.tsx index 99f0c9ef..ff0ec358 100644 --- a/src/components/templates/loginTemplate/index.tsx +++ b/src/components/templates/loginTemplate/index.tsx @@ -1,11 +1,24 @@ import Image from 'next/image'; import Header from '../../organisms/header'; import LoginForm from '../../organisms/loginForm'; -import { LoginProps } from './interface'; +import { LoginProps, Notice } from './interface'; import RoundButton from '../../atomos/roundButton'; -import { REPORT_FORM_URL } from '../../../utils/constants'; +import { REPORT_FORM_URL, SERVER_URI } from '../../../utils/constants'; +import { useEffect, useState } from 'react'; const LoginTemplate: React.FunctionComponent<LoginProps> = ({ version }: LoginProps) => { + const [notice, setNotice] = useState<Notice[]>([]); + + useEffect(() => { + const asyncTask = async () => { + const req = await fetch(`${SERVER_URI}/Settings/Notice`); + const res = await req.json(); + setNotice(res); + }; + + asyncTask(); + }, []); + const openReportForm = () => { window.open(REPORT_FORM_URL); }; @@ -30,14 +43,12 @@ const LoginTemplate: React.FunctionComponent<LoginProps> = ({ version }: LoginPr </div> <LoginForm /> <hr /> - <div className="box-border rounded-2xl border-2 border-border my-2 px-3 py-2"> - <span className="font-bold">お知らせ (2024/7/5追加)</span><br /> - 只今、一覧表での登録情報の検索の際に<br /> - 市町村を指定すると、今年の2~4月の情報が<br /> - 出ないことがあります。<br /> - 復旧までしばらくお待ちください。<br /> - ご迷惑をおかけしております。<br /> - </div> + {notice.map((n, i) => ( + <div className="box-border rounded-2xl border-2 border-border my-2 px-3 py-2" key={"notice_" + i}> + <span className="font-bold">{n.title}</span><br /> + <span className="whitespace-pre-wrap">{n.content}</span> + </div> + ))} <div className="box-border rounded-2xl border-2 border-border my-2 px-3 py-2"> 改修中のため入力項目等が<br /> 変更になる場合があります。<br /> @@ -57,7 +68,7 @@ const LoginTemplate: React.FunctionComponent<LoginProps> = ({ version }: LoginPr </div> </div> <div className='pt-8 text-center'> - © 2019-2022 National Institute of Technology, Gifu College GIS Team + © 2019-2025 National Institute of Technology, Gifu College GIS Team </div> </div> </div> diff --git a/src/components/templates/loginTemplate/interface.ts b/src/components/templates/loginTemplate/interface.ts index 423dcf28..3e4a1d1e 100644 --- a/src/components/templates/loginTemplate/interface.ts +++ b/src/components/templates/loginTemplate/interface.ts @@ -3,3 +3,8 @@ import { VersionInformation } from '../../../utils/version'; export interface LoginProps { version: VersionInformation; } + +export interface Notice { + title: string; + content: string; +} \ No newline at end of file diff --git a/src/components/templates/noticeSettingsTemplate/index.tsx b/src/components/templates/noticeSettingsTemplate/index.tsx new file mode 100644 index 00000000..30331359 --- /dev/null +++ b/src/components/templates/noticeSettingsTemplate/index.tsx @@ -0,0 +1,206 @@ +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useState } from 'react'; +import { useCurrentUser } from '../../../hooks/useCurrentUser'; +import { SERVER_URI } from '../../../utils/constants'; +import { getAccessToken } from '../../../utils/currentUser'; +import FooterAdjustment from '../../atomos/footerAdjustment'; +import RoundButton from '../../atomos/roundButton'; +import Footer from '../../organisms/footer'; +import Header from '../../organisms/header'; +import { Notice } from '../loginTemplate/interface'; +import TextInput from '../../atomos/TextInput'; +import TextAreaInput from '../../atomos/textAreaInput'; + +const NoticeSettingsTemplate: React.FunctionComponent = () => { + const router = useRouter(); + const { currentUser } = useCurrentUser(); + + const [notice, setNotice] = useState<Notice[]>([]); + + const [buttonDisabled, setButtonDisabled] = useState(false); + const [inputsMessage, setInputsMessage] = useState(''); + const [, setMessageDeleteTimerId] = useState<NodeJS.Timeout | null>(null); + const [, setInputsMessageDeleteTimerId] = useState<NodeJS.Timeout | null>(null); + + useEffect(() => { + if (currentUser == null) return; + + if (currentUser.userDepartment !== 'K') { + alert('権限エラー\nこのページにアクセスする権限がありません。'); + router.push('/map'); + return; + } + + const asyncTask = async () => { + const req = await fetch(`${SERVER_URI}/Settings/Notice`); + const res = await req.json(); + setNotice(res); + }; + + asyncTask(); + + return () => { + setMessageDeleteTimerId((id) => { + if (id != null) { + clearTimeout(id); + } + return id; + }); + }; + }, []); + + const onUpdateClicked = useCallback(async () => { + setButtonDisabled(true); + + const req = await fetch(`${SERVER_URI}/Settings/Notice`, { + method: 'POST', + headers: { + 'X-Access-Token': getAccessToken(), + }, + body: JSON.stringify({ + data: notice, + }), + }); + + const message = req.ok ? '設定を更新しました。' : `エラーが発生しました。(${req.status})`; + setInputsMessageDeleteTimerId((id) => { + setInputsMessage(message); + + if (id != null) clearTimeout(id); + + return setTimeout(() => { + setInputsMessage(''); + }, 4000); + }); + setButtonDisabled(false); + }, [notice]); + + const onNewClicked = useCallback(() => { + const newNotice = [...notice]; + newNotice.push({ + title: '', + content: '', + }); + setNotice(newNotice); + }, [notice]); + + const onDeleteClicked = useCallback((index: number) => { + const newNotice = [...notice]; + newNotice.splice(index, 1); + setNotice(newNotice); + }, [notice]); + + const onClickUp = useCallback((index: number) => { + if (index == 0) return; + + const newNotice = [...notice]; + const tmp = newNotice[index - 1]; + newNotice[index - 1] = newNotice[index]; + newNotice[index] = tmp; + setNotice(newNotice); + }, [notice]); + const onClickDown = useCallback((index: number) => { + if (index + 1 >= notice.length) return; + + const newNotice = [...notice]; + const tmp = newNotice[index + 1]; + newNotice[index + 1] = newNotice[index]; + newNotice[index] = tmp; + setNotice(newNotice); + }, [notice]); + + const onChangeTitle = useCallback((index: number, value: string) => { + const newNotice = [...notice]; + newNotice[index].title = value; + setNotice(newNotice); + }, [notice]); + const onChangeBody = useCallback((index: number) => { + const body = document.getElementById(`body_${index}`) as HTMLTextAreaElement; + if (body == null) return; + + const newNotice = [...notice]; + newNotice[index].content = body.value; + setNotice(newNotice); + }, [notice]); + + return ( + <div> + <Header color='primary'>お知らせ設定</Header> + {notice.length === 0 ? ( + <div className='mx-auto w-full max-w-[400px] bg-background py-3'> + <div className='text-xl font-bold'> + お知らせはありません。 + </div> + </div> + ) : <></>} + {notice.map((n, i, a) => ( + <div className='mx-auto w-full max-w-[400px] bg-background py-3' key={`notice_${i}`}> + <div className='box-border w-full rounded-xl border-2 border-solid border-border py-[10px] px-2'> + <div className='text-2xl font-bold flex flex-wrap items-center'> + <div className="mx-3">お知らせ{i + 1}</div> + </div> + <div className='flex flex-wrap items-center'> + <div className="w-32"> + <RoundButton color='danger' onClick={() => onDeleteClicked(i)}>削除</RoundButton> + </div> + <div className="w-16 mx-2"> + <RoundButton color="accent" onClick={() => onClickUp(i)} disabled={i == 0}>↑</RoundButton> + </div> + <div className="w-16 mx-2"> + <RoundButton color="accent" onClick={() => onClickDown(i)} disabled={(i + 1) >= a.length}>↓</RoundButton> + </div> + </div> + <div className='m-[15px]'> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + お知らせタイトル + <TextInput + type='text' + id={`title_${i}`} + name={`title_${i}`} + onChange={(e) => onChangeTitle(i, e.target.value)} + value={n.title} + /> + </div> + </div> + <div className='m-[15px]'> + <div className='mt-[15px] mb-[5px] w-full text-justify text-lg font-bold text-text'> + お知らせ本文 + </div> + <div> + <TextAreaInput + id={`body_${i}`} + rows={5} + onChange={() => onChangeBody(i)} + value={n.content} + /> + </div> + </div> + </div> + </div> + ))} + <div className='mx-auto w-full max-w-[400px] bg-background py-3'> + <RoundButton color='excel' onClick={onNewClicked} disabled={buttonDisabled}> + 新規追加 + </RoundButton> + </div> + <div className='mx-auto w-full max-w-[400px] bg-background py-3'> + <RoundButton color='primary' onClick={onUpdateClicked} disabled={buttonDisabled}> + 保存 + </RoundButton> + <div className='pt-3 text-center'> + <span>{inputsMessage}</span> + </div> + </div> + <FooterAdjustment /> + <div className='fixed bottom-0 w-full'> + <Footer> + <RoundButton onClick={() => router.push('/settings')} color='accent'> + < 戻る + </RoundButton> + </Footer> + </div> + </div> + ); +}; + +export default NoticeSettingsTemplate; \ No newline at end of file diff --git a/src/components/templates/settingsTemplate/index.tsx b/src/components/templates/settingsTemplate/index.tsx index 59a8d0ec..de9f216e 100644 --- a/src/components/templates/settingsTemplate/index.tsx +++ b/src/components/templates/settingsTemplate/index.tsx @@ -45,6 +45,16 @@ const SettingsTemplate: React.FunctionComponent = () => { path: '/settings/city', title: '市町村設定', }); + + s.push({ + path: '/dummy-spacer-1', + title: '', + }); + + s.push({ + path: '/settings/notice', + title: 'お知らせ設定', + }); } setSettings(s); diff --git a/src/pages/settings/notice.tsx b/src/pages/settings/notice.tsx new file mode 100644 index 00000000..f5da2b83 --- /dev/null +++ b/src/pages/settings/notice.tsx @@ -0,0 +1,14 @@ +import { NextPage } from 'next'; +import { useRequireLogin } from '../../hooks/useLogin'; +import NoticeSettingsTemplate from '../../components/templates/noticeSettingsTemplate'; + +const NoticeSettingsPage: NextPage = () => { + useRequireLogin(); + return ( + <> + <NoticeSettingsTemplate /> + </> + ); +}; + +export default NoticeSettingsPage; From b245ba75792a7f452a3c8d501a3637c2b5d380de Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Mon, 20 Jan 2025 00:47:47 +0900 Subject: [PATCH 10/15] =?UTF-8?q?fix:=20=E3=83=A1=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E7=95=AA=E5=8F=B7=E8=A1=A8=E7=A4=BA=E3=81=AE=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/static/leaflet.css | 1 - src/components/organisms/mapBase/base.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/static/leaflet.css b/public/static/leaflet.css index f1a5f1ba..c4debfde 100644 --- a/public/static/leaflet.css +++ b/public/static/leaflet.css @@ -678,7 +678,6 @@ svg.leaflet-image-layer.leaflet-interactive path { .leaflet-div-icon { background: #fff; - border: 1px solid #666; display: flex; justify-content: center; } diff --git a/src/components/organisms/mapBase/base.tsx b/src/components/organisms/mapBase/base.tsx index 1b9399f0..35b812e4 100644 --- a/src/components/organisms/mapBase/base.tsx +++ b/src/components/organisms/mapBase/base.tsx @@ -423,14 +423,14 @@ const MapBase_: React.FunctionComponent<MapBaseProps> = (props) => { const newMeshData = data[k].filter(v=>!featureIDs[k0].includes(v.id)); const IDs = data[k].map(v=>v.id); const deleteMeshIDs = featureIDs[k0].filter(id=>!IDs.includes(id)); - // 新しいメッシュを描画する + // 新しいメッシュを描画する#d14b02 #65db56 newMeshData.forEach(v => { const po = L.polygon(v.coordinates, polygonParam(k, v.fillOpacity)); const gr: (L.Marker | L.Polygon)[] = [po]; if (v.fillOpacity === undefined) { const ma = L.marker(po.getBounds().getCenter(), { icon: L.divIcon({ - html: '<div style="font-weight: bold; font-size: 1.2em; word-break: keep-all;">' + (v.name == null ? "" : v.name) + '</div>' + html: '<div style="font-weight: bold; font-size: 1.2em; word-break: keep-all; color: #000; -webkit-text-stroke: 0.5px ' + (k === "vaccine" ? "#d14b02" : "#65db56") + ';">' + (v.name == null ? "" : v.name) + '</div>', }) }); gr.push(ma); From ad776775c81e90c80b99f866a11ddfdb7d36ebba Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Mon, 20 Jan 2025 00:49:14 +0900 Subject: [PATCH 11/15] =?UTF-8?q?chore:=20=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=83=88=E3=83=AD=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/history.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/history.md b/public/history.md index 105e0248..8b4cc23b 100644 --- a/public/history.md +++ b/public/history.md @@ -1,3 +1,12 @@ +### Version 2.5.0α + +- わなアイコンの種類を追加しました。 +- 歯列画像を2枚まで追加できるようになりました。 +- 作業日報の入力項目を変更しました。 +- 作業日報AをPDFファイルとして出力できるようになりました。 +- メッシュ番号表示のデザインを調整しました。 + + ### Version 2.4.2 - 情報が正常に検索されない問題を修正しました。 From a72ec7c685b11ccef6f8252ca998cd06d8469cd8 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Mon, 20 Jan 2025 00:52:41 +0900 Subject: [PATCH 12/15] =?UTF-8?q?fix:=20=E9=96=8B=E7=99=BA=E7=89=88?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/organisms/loginForm/index.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/organisms/loginForm/index.tsx b/src/components/organisms/loginForm/index.tsx index 5891d070..02ba23c9 100644 --- a/src/components/organisms/loginForm/index.tsx +++ b/src/components/organisms/loginForm/index.tsx @@ -7,7 +7,7 @@ import { SERVER_URI } from '../../../utils/constants'; import RoundButton from '../../atomos/roundButton'; import TextInput from '../../atomos/TextInput'; import { setCookie } from 'nookies'; -import { confirm } from '../../../utils/modal'; +import { alert } from '../../../utils/modal'; import * as Sentry from '@sentry/nextjs'; const LoginForm: React.FunctionComponent = () => { @@ -19,13 +19,21 @@ const LoginForm: React.FunctionComponent = () => { useEffect(() => { const asyncTask = async () => { // 開発用サーバーだった場合には通知を表示する - if ( + /*if ( document.domain.toLowerCase().endsWith('.prsvr.net') || document.domain.toLocaleLowerCase().endsWith('.gifu-nct.ac.jp') ) { if (await confirm('このサイトは開発版です。\n安定動作版のサイトへ移動しますか?')) { location.href = 'https://boar-map.gifugis.jp/login'; } + }*/ + + // 開発版サーバーの場合の通知はOKメッセージだけにする。 (2025/1/20暫定対応) + if ( + document.domain.toLowerCase().endsWith('.prsvr.net') || + document.domain.toLocaleLowerCase().endsWith('.gifu-nct.ac.jp') + ) { + await alert('このサイトは開発版です。\n安定動作版と異なり、正常に動作しない場合があります。'); } }; asyncTask(); From bbbfcbe14ecfc77b718a9d58c6bba80e3679ab58 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Mon, 20 Jan 2025 01:19:33 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20=E6=94=AF=E9=83=A8=E5=90=8D?= =?UTF-8?q?=E3=81=AA=E3=81=A9=E3=82=92=E8=87=AA=E7=94=B1=E5=85=A5=E5=8A=9B?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organisms/reportInfoForm/index.tsx | 40 ++++++++++++++----- .../organisms/reportInfoView/index.tsx | 1 + 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/organisms/reportInfoForm/index.tsx b/src/components/organisms/reportInfoForm/index.tsx index 726c078d..c90b129d 100644 --- a/src/components/organisms/reportInfoForm/index.tsx +++ b/src/components/organisms/reportInfoForm/index.tsx @@ -22,8 +22,10 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp const fetchData = () => { const form = document.getElementById('form-report') as HTMLFormElement; const area = form.area.options[form.area.selectedIndex].value as string; - const branch = form.branch.options[form.branch.selectedIndex].value as string; - const name = form.person_name.options[form.person_name.selectedIndex].value as string; + // const branch = form.branch.options[form.branch.selectedIndex].value as string; + const branch = form.branch.value as string; + const name = form.person_name.value as string; + // const name = form.person_name.options[form.person_name.selectedIndex].value as string; const time_start = (document.getElementById('worktime_start') as HTMLInputElement) .value as string; const time_end = (document.getElementById('worktime_end') as HTMLInputElement) @@ -69,7 +71,7 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp const tool_gun = (document.getElementById('report_b_gun') as HTMLInputElement).checked; const tool_other = (document.getElementById('report_b_other') as HTMLInputElement).checked; - const tool_other_content = (document.getElementById('report_b_other_tool') as HTMLInputElement).value; + const tool_other_content = tool_other ? (document.getElementById('report_b_other_tool') as HTMLInputElement).value : ""; const tool_str = `${tool_elec ? '電気とめさし器' : ''}/${tool_gun ? '銃' : ''}/${tool_other ? 'その他' : ''}\n${tool_other_content}`; @@ -114,8 +116,10 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp const form = document.getElementById('form-report') as HTMLFormElement; const area = form.area.options[form.area.selectedIndex].value as string; - const branch = form.branch.options[form.branch.selectedIndex].value as string; - const name = form.person_name.options[form.person_name.selectedIndex].value as string; + // const branch = form.branch.options[form.branch.selectedIndex].value as string; + const branch = form.branch.value as string; + const name = form.person_name.value as string; + // const name = form.person_name.options[form.person_name.selectedIndex].value as string; const time_start = (document.getElementById('worktime_start') as HTMLInputElement) .value as string; const time_end = (document.getElementById('worktime_end') as HTMLInputElement) @@ -130,12 +134,12 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp if (branch == '' || branch == '(上を選択してください。)') { valid = false; - updateError('branch', '選択されていません'); + updateError('branch', '入力されていません'); } if (name == '' || name == '(上を選択してください。)') { valid = false; - updateError('person_name', '選択されていません'); + updateError('person_name', '入力されていません'); } if (!time_start || !time_end) { @@ -203,7 +207,7 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp form.area.selectedIndex = areaList.indexOf(props.featureInfo.properties.地域); - // 支部名のリストを取得 + /* // 支部名のリストを取得 const res = await fetch( SERVER_URI + '/Report/GetBranches?' + @@ -247,7 +251,7 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp if (!props.featureInfo.properties.氏名) return; - form.person_name.selectedIndex = nameList.indexOf(props.featureInfo.properties.氏名); + form.person_name.selectedIndex = nameList.indexOf(props.featureInfo.properties.氏名); */ }; fetchDefault(); }, []); @@ -333,6 +337,22 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp error={errors.area} /> <InfoInput + title="所属支部名" + id="branch" + type='text' + required={true} + error={errors.branch} + defaultValue={featureValueOrUndefined('所属支部名')} + /> + <InfoInput + title='氏名' + id='person_name' + type='text' + required={true} + error={errors.person_name} + defaultValue={featureValueOrUndefined('氏名')} + /> + {/*<InfoInput title='所属支部名' id='branch' type='select' @@ -349,7 +369,7 @@ const ReportInfoForm = React.forwardRef<FeatureEditorHandler, ReportInfoFormProp required={true} error={errors.person_name} onChange={() => updateError('person_name', undefined)} - /> + />*/} <InfoInput title='わなの場所' subtitle='市町村・字' diff --git a/src/components/organisms/reportInfoView/index.tsx b/src/components/organisms/reportInfoView/index.tsx index 675e02f7..95a0e7dc 100644 --- a/src/components/organisms/reportInfoView/index.tsx +++ b/src/components/organisms/reportInfoView/index.tsx @@ -161,6 +161,7 @@ const ReportInfoView: React.FunctionComponent<ReportInfoViewProps> = ({ type='location' data={{ lat: detail.geometry.coordinates[1], lng: detail.geometry.coordinates[0] }} /> + <InfoDiv title='地域' type='text' data={detail.properties.地域} /> <InfoDiv title='所属支部名' type='text' data={detail.properties.所属支部名} /> <InfoDiv title='氏名' type='text' data={detail.properties.氏名} /> <InfoDiv title="わなの場所(市町村・字)" data={detail.properties.市町村字} /> From 0c0160466cb2db30b2705b90a13fa8a987d7699d Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Thu, 23 Jan 2025 00:31:45 +0900 Subject: [PATCH 14/15] =?UTF-8?q?feat:=20=E3=83=9E=E3=83=8B=E3=83=A5?= =?UTF-8?q?=E3=82=A2=E3=83=AB=E3=83=AA=E3=83=B3=E3=82=AF=E3=82=92=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=20(2025/1/23=E6=9A=AB=E5=AE=9A=E7=89=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 41b8433f..c7badb01 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -20,7 +20,7 @@ const getServerURI = (): string => { }; export const REPORT_FORM_URL = "https://boar-map.gifugis.jp/media/R5_ReportForm_FaxPaper_20231016.pdf"; -export const MANUAL_URL = "https://boar-map.gifugis.jp/media/manual_20221214.pdf"; +export const MANUAL_URL = "https://boar-map.gifugis.jp/media/manual_20250123.pdf"; export const SERVER_URI = getServerURI(); From 2513507e549732d1cc6931355a326c6bc54d0407 Mon Sep 17 00:00:00 2001 From: Junki Tomatsu <me@db0.jp> Date: Tue, 28 Jan 2025 18:26:21 +0900 Subject: [PATCH 15/15] =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E7=95=AA=E5=8F=B7=E3=81=AE=E3=83=8F=E3=82=A4=E3=83=95=E3=83=B3?= =?UTF-8?q?=E3=81=A7=E6=94=B9=E8=A1=8C=E3=81=97=E3=81=AA=E3=81=84=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/organisms/mapBase/base.tsx | 2 +- src/components/organisms/selectionMap/base.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/organisms/mapBase/base.tsx b/src/components/organisms/mapBase/base.tsx index 35b812e4..afb654a1 100644 --- a/src/components/organisms/mapBase/base.tsx +++ b/src/components/organisms/mapBase/base.tsx @@ -430,7 +430,7 @@ const MapBase_: React.FunctionComponent<MapBaseProps> = (props) => { if (v.fillOpacity === undefined) { const ma = L.marker(po.getBounds().getCenter(), { icon: L.divIcon({ - html: '<div style="font-weight: bold; font-size: 1.2em; word-break: keep-all; color: #000; -webkit-text-stroke: 0.5px ' + (k === "vaccine" ? "#d14b02" : "#65db56") + ';">' + (v.name == null ? "" : v.name) + '</div>', + html: '<div style="white-space: nowrap; font-weight: bold; font-size: 1.2em; word-break: keep-all; color: #000; -webkit-text-stroke: 0.5px ' + (k === "vaccine" ? "#d14b02" : "#65db56") + ';">' + (v.name == null ? "" : v.name) + '</div>', }) }); gr.push(ma); diff --git a/src/components/organisms/selectionMap/base.tsx b/src/components/organisms/selectionMap/base.tsx index 85a69c15..97b40353 100644 --- a/src/components/organisms/selectionMap/base.tsx +++ b/src/components/organisms/selectionMap/base.tsx @@ -416,7 +416,7 @@ const SelectionMap_: React.FunctionComponent<SelectionMapProps> = (props) => { if (v.fillOpacity === undefined) { const ma = L.marker(po.getBounds().getCenter(), { icon: L.divIcon({ - html: '<div style="font-weight: bold; font-size: 1.2em; word-break: keep-all;">' + (v.name == null ? "" : v.name) + '</div>' + html: '<div style="white-space: nowrap; font-weight: bold; font-size: 1.2em; word-break: keep-all;">' + (v.name == null ? "" : v.name) + '</div>' }) }); gr.push(ma);