diff --git a/go.work b/go.work new file mode 100644 index 0000000..d4bf84b --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.21 + +use ..\fiesta-backend\backend_fiesta diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..0966965 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,156 @@ +cloud.google.com/go/accessapproval v1.7.1/go.mod h1:JYczztsHRMK7NTXb6Xw+dwbs/WnOJxbo/2mTI+Kgg68= +cloud.google.com/go/accesscontextmanager v1.8.1/go.mod h1:JFJHfvuaTC+++1iL1coPiG1eu5D24db2wXCDWDjIrxo= +cloud.google.com/go/aiplatform v1.48.0/go.mod h1:Iu2Q7sC7QGhXUeOhAj/oCK9a+ULz1O4AotZiqjQ8MYA= +cloud.google.com/go/analytics v0.21.3/go.mod h1:U8dcUtmDmjrmUTnnnRnI4m6zKn/yaA5N9RlEkYFHpQo= +cloud.google.com/go/apigateway v1.6.1/go.mod h1:ufAS3wpbRjqfZrzpvLC2oh0MFlpRJm2E/ts25yyqmXA= +cloud.google.com/go/apigeeconnect v1.6.1/go.mod h1:C4awq7x0JpLtrlQCr8AzVIzAaYgngRqWf9S5Uhg+wWs= +cloud.google.com/go/apigeeregistry v0.7.1/go.mod h1:1XgyjZye4Mqtw7T9TsY4NW10U7BojBvG4RMD+vRDrIw= +cloud.google.com/go/appengine v1.8.1/go.mod h1:6NJXGLVhZCN9aQ/AEDvmfzKEfoYBlfB80/BHiKVputY= +cloud.google.com/go/area120 v0.8.1/go.mod h1:BVfZpGpB7KFVNxPiQBuHkX6Ed0rS51xIgmGyjrAfzsg= +cloud.google.com/go/artifactregistry v1.14.1/go.mod h1:nxVdG19jTaSTu7yA7+VbWL346r3rIdkZ142BSQqhn5E= +cloud.google.com/go/asset v1.14.1/go.mod h1:4bEJ3dnHCqWCDbWJ/6Vn7GVI9LerSi7Rfdi03hd+WTQ= +cloud.google.com/go/assuredworkloads v1.11.1/go.mod h1:+F04I52Pgn5nmPG36CWFtxmav6+7Q+c5QyJoL18Lry0= +cloud.google.com/go/automl v1.13.1/go.mod h1:1aowgAHWYZU27MybSCFiukPO7xnyawv7pt3zK4bheQE= +cloud.google.com/go/baremetalsolution v1.1.1/go.mod h1:D1AV6xwOksJMV4OSlWHtWuFNZZYujJknMAP4Qa27QIA= +cloud.google.com/go/batch v1.3.1/go.mod h1:VguXeQKXIYaeeIYbuozUmBR13AfL4SJP7IltNPS+A4A= +cloud.google.com/go/beyondcorp v1.0.0/go.mod h1:YhxDWw946SCbmcWo3fAhw3V4XZMSpQ/VYfcKGAEU8/4= +cloud.google.com/go/bigquery v1.53.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= +cloud.google.com/go/billing v1.16.0/go.mod h1:y8vx09JSSJG02k5QxbycNRrN7FGZB6F3CAcgum7jvGA= +cloud.google.com/go/binaryauthorization v1.6.1/go.mod h1:TKt4pa8xhowwffiBmbrbcxijJRZED4zrqnwZ1lKH51U= +cloud.google.com/go/certificatemanager v1.7.1/go.mod h1:iW8J3nG6SaRYImIa+wXQ0g8IgoofDFRp5UMzaNk1UqI= +cloud.google.com/go/channel v1.16.0/go.mod h1:eN/q1PFSl5gyu0dYdmxNXscY/4Fi7ABmeHCJNf/oHmc= +cloud.google.com/go/cloudbuild v1.13.0/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= +cloud.google.com/go/clouddms v1.6.1/go.mod h1:Ygo1vL52Ov4TBZQquhz5fiw2CQ58gvu+PlS6PVXCpZI= +cloud.google.com/go/cloudtasks v1.12.1/go.mod h1:a9udmnou9KO2iulGscKR0qBYjreuX8oHwpmFsKspEvM= +cloud.google.com/go/contactcenterinsights v1.10.0/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= +cloud.google.com/go/container v1.24.0/go.mod h1:lTNExE2R7f+DLbAN+rJiKTisauFCaoDq6NURZ83eVH4= +cloud.google.com/go/containeranalysis v0.10.1/go.mod h1:Ya2jiILITMY68ZLPaogjmOMNkwsDrWBSTyBubGXO7j0= +cloud.google.com/go/datacatalog v1.16.0/go.mod h1:d2CevwTG4yedZilwe+v3E3ZBDRMobQfSG/a6cCCN5R4= +cloud.google.com/go/dataflow v0.9.1/go.mod h1:Wp7s32QjYuQDWqJPFFlnBKhkAtiFpMTdg00qGbnIHVw= +cloud.google.com/go/dataform v0.8.1/go.mod h1:3BhPSiw8xmppbgzeBbmDvmSWlwouuJkXsXsb8UBih9M= +cloud.google.com/go/datafusion v1.7.1/go.mod h1:KpoTBbFmoToDExJUso/fcCiguGDk7MEzOWXUsJo0wsI= +cloud.google.com/go/datalabeling v0.8.1/go.mod h1:XS62LBSVPbYR54GfYQsPXZjTW8UxCK2fkDciSrpRFdY= +cloud.google.com/go/dataplex v1.9.0/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataproc/v2 v2.0.1/go.mod h1:7Ez3KRHdFGcfY7GcevBbvozX+zyWGcwLJvvAMwCaoZ4= +cloud.google.com/go/dataqna v0.8.1/go.mod h1:zxZM0Bl6liMePWsHA8RMGAfmTG34vJMapbHAxQ5+WA8= +cloud.google.com/go/datastore v1.13.0/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70= +cloud.google.com/go/datastream v1.10.0/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= +cloud.google.com/go/deploy v1.13.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g= +cloud.google.com/go/dialogflow v1.40.0/go.mod h1:L7jnH+JL2mtmdChzAIcXQHXMvQkE3U4hTaNltEuxXn4= +cloud.google.com/go/dlp v1.10.1/go.mod h1:IM8BWz1iJd8njcNcG0+Kyd9OPnqnRNkDV8j42VT5KOI= +cloud.google.com/go/documentai v1.22.0/go.mod h1:yJkInoMcK0qNAEdRnqY/D5asy73tnPe88I1YTZT+a8E= +cloud.google.com/go/domains v0.9.1/go.mod h1:aOp1c0MbejQQ2Pjf1iJvnVyT+z6R6s8pX66KaCSDYfE= +cloud.google.com/go/edgecontainer v1.1.1/go.mod h1:O5bYcS//7MELQZs3+7mabRqoWQhXCzenBu0R8bz2rwk= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.6.2/go.mod h1:T2tB6tX+TRak7i88Fb2N9Ok3PvY3UNbUsMag9/BARh4= +cloud.google.com/go/eventarc v1.13.0/go.mod h1:mAFCW6lukH5+IZjkvrEss+jmt2kOdYlN8aMx3sRJiAI= +cloud.google.com/go/filestore v1.7.1/go.mod h1:y10jsorq40JJnjR/lQ8AfFbbcGlw3g+Dp8oN7i7FjV4= +cloud.google.com/go/functions v1.15.1/go.mod h1:P5yNWUTkyU+LvW/S9O6V+V423VZooALQlqoXdoPz5AE= +cloud.google.com/go/gkebackup v1.3.0/go.mod h1:vUDOu++N0U5qs4IhG1pcOnD1Mac79xWy6GoBFlWCWBU= +cloud.google.com/go/gkeconnect v0.8.1/go.mod h1:KWiK1g9sDLZqhxB2xEuPV8V9NYzrqTUmQR9shJHpOZw= +cloud.google.com/go/gkehub v0.14.1/go.mod h1:VEXKIJZ2avzrbd7u+zeMtW00Y8ddk/4V9511C9CQGTY= +cloud.google.com/go/gkemulticloud v1.0.0/go.mod h1:kbZ3HKyTsiwqKX7Yw56+wUGwwNZViRnxWK2DVknXWfw= +cloud.google.com/go/gsuiteaddons v1.6.1/go.mod h1:CodrdOqRZcLp5WOwejHWYBjZvfY0kOphkAKpF/3qdZY= +cloud.google.com/go/iap v1.8.1/go.mod h1:sJCbeqg3mvWLqjZNsI6dfAtbbV1DL2Rl7e1mTyXYREQ= +cloud.google.com/go/ids v1.4.1/go.mod h1:np41ed8YMU8zOgv53MMMoCntLTn2lF+SUzlM+O3u/jw= +cloud.google.com/go/iot v1.7.1/go.mod h1:46Mgw7ev1k9KqK1ao0ayW9h0lI+3hxeanz+L1zmbbbk= +cloud.google.com/go/kms v1.15.0/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= +cloud.google.com/go/language v1.10.1/go.mod h1:CPp94nsdVNiQEt1CNjF5WkTcisLiHPyIbMhvR8H2AW0= +cloud.google.com/go/lifesciences v0.9.1/go.mod h1:hACAOd1fFbCGLr/+weUKRAJas82Y4vrL3O5326N//Wc= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/managedidentities v1.6.1/go.mod h1:h/irGhTN2SkZ64F43tfGPMbHnypMbu4RB3yl8YcuEak= +cloud.google.com/go/maps v1.4.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s= +cloud.google.com/go/mediatranslation v0.8.1/go.mod h1:L/7hBdEYbYHQJhX2sldtTO5SZZ1C1vkapubj0T2aGig= +cloud.google.com/go/memcache v1.10.1/go.mod h1:47YRQIarv4I3QS5+hoETgKO40InqzLP6kpNLvyXuyaA= +cloud.google.com/go/metastore v1.12.0/go.mod h1:uZuSo80U3Wd4zi6C22ZZliOUJ3XeM/MlYi/z5OAOWRA= +cloud.google.com/go/monitoring v1.15.1/go.mod h1:lADlSAlFdbqQuwwpaImhsJXu1QSdd3ojypXrFSMr2rM= +cloud.google.com/go/networkconnectivity v1.12.1/go.mod h1:PelxSWYM7Sh9/guf8CFhi6vIqf19Ir/sbfZRUwXh92E= +cloud.google.com/go/networkmanagement v1.8.0/go.mod h1:Ho/BUGmtyEqrttTgWEe7m+8vDdK74ibQc+Be0q7Fof0= +cloud.google.com/go/networksecurity v0.9.1/go.mod h1:MCMdxOKQ30wsBI1eI659f9kEp4wuuAueoC9AJKSPWZQ= +cloud.google.com/go/notebooks v1.9.1/go.mod h1:zqG9/gk05JrzgBt4ghLzEepPHNwE5jgPcHZRKhlC1A8= +cloud.google.com/go/optimization v1.4.1/go.mod h1:j64vZQP7h9bO49m2rVaTVoNM0vEBEN5eKPUPbZyXOrk= +cloud.google.com/go/orchestration v1.8.1/go.mod h1:4sluRF3wgbYVRqz7zJ1/EUNc90TTprliq9477fGobD8= +cloud.google.com/go/orgpolicy v1.11.1/go.mod h1:8+E3jQcpZJQliP+zaFfayC2Pg5bmhuLK755wKhIIUCE= +cloud.google.com/go/osconfig v1.12.1/go.mod h1:4CjBxND0gswz2gfYRCUoUzCm9zCABp91EeTtWXyz0tE= +cloud.google.com/go/oslogin v1.10.1/go.mod h1:x692z7yAue5nE7CsSnoG0aaMbNoRJRXO4sn73R+ZqAs= +cloud.google.com/go/phishingprotection v0.8.1/go.mod h1:AxonW7GovcA8qdEk13NfHq9hNx5KPtfxXNeUxTDxB6I= +cloud.google.com/go/policytroubleshooter v1.8.0/go.mod h1:tmn5Ir5EToWe384EuboTcVQT7nTag2+DuH3uHmKd1HU= +cloud.google.com/go/privatecatalog v0.9.1/go.mod h1:0XlDXW2unJXdf9zFz968Hp35gl/bhF4twwpXZAW50JA= +cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.2/go.mod h1:kR0KjsJS7Jt1YSyWFkseQ756D45kaYNTlDPPaRAvDBU= +cloud.google.com/go/recommendationengine v0.8.1/go.mod h1:MrZihWwtFYWDzE6Hz5nKcNz3gLizXVIDI/o3G1DLcrE= +cloud.google.com/go/recommender v1.10.1/go.mod h1:XFvrE4Suqn5Cq0Lf+mCP6oBHD/yRMA8XxP5sb7Q7gpA= +cloud.google.com/go/redis v1.13.1/go.mod h1:VP7DGLpE91M6bcsDdMuyCm2hIpB6Vp2hI090Mfd1tcg= +cloud.google.com/go/resourcemanager v1.9.1/go.mod h1:dVCuosgrh1tINZ/RwBufr8lULmWGOkPS8gL5gqyjdT8= +cloud.google.com/go/resourcesettings v1.6.1/go.mod h1:M7mk9PIZrC5Fgsu1kZJci6mpgN8o0IUzVx3eJU3y4Jw= +cloud.google.com/go/retail v1.14.1/go.mod h1:y3Wv3Vr2k54dLNIrCzenyKG8g8dhvhncT2NcNjb/6gE= +cloud.google.com/go/run v1.2.0/go.mod h1:36V1IlDzQ0XxbQjUx6IYbw8H3TJnWvhii963WW3B/bo= +cloud.google.com/go/scheduler v1.10.1/go.mod h1:R63Ldltd47Bs4gnhQkmNDse5w8gBRrhObZ54PxgR2Oo= +cloud.google.com/go/secretmanager v1.11.1/go.mod h1:znq9JlXgTNdBeQk9TBW/FnR/W4uChEKGeqQWAJ8SXFw= +cloud.google.com/go/security v1.15.1/go.mod h1:MvTnnbsWnehoizHi09zoiZob0iCHVcL4AUBj76h9fXA= +cloud.google.com/go/securitycenter v1.23.0/go.mod h1:8pwQ4n+Y9WCWM278R8W3nF65QtY172h4S8aXyI9/hsQ= +cloud.google.com/go/servicedirectory v1.11.0/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ= +cloud.google.com/go/shell v1.7.1/go.mod h1:u1RaM+huXFaTojTbW4g9P5emOrrmLE69KrxqQahKn4g= +cloud.google.com/go/spanner v1.47.0/go.mod h1:IXsJwVW2j4UKs0eYDqodab6HgGuA1bViSqW4uH9lfUI= +cloud.google.com/go/speech v1.19.0/go.mod h1:8rVNzU43tQvxDaGvqOhpDqgkJTFowBpDvCJ14kGlJYo= +cloud.google.com/go/storagetransfer v1.10.0/go.mod h1:DM4sTlSmGiNczmV6iZyceIh2dbs+7z2Ayg6YAiQlYfA= +cloud.google.com/go/talent v1.6.2/go.mod h1:CbGvmKCG61mkdjcqTcLOkb2ZN1SrQI8MDyma2l7VD24= +cloud.google.com/go/texttospeech v1.7.1/go.mod h1:m7QfG5IXxeneGqTapXNxv2ItxP/FS0hCZBwXYqucgSk= +cloud.google.com/go/tpu v1.6.1/go.mod h1:sOdcHVIgDEEOKuqUoi6Fq53MKHJAtOwtz0GuKsWSH3E= +cloud.google.com/go/trace v1.10.1/go.mod h1:gbtL94KE5AJLH3y+WVpfWILmqgc6dXcqgNXdOPAQTYk= +cloud.google.com/go/translate v1.8.2/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNWRljklu5RHs= +cloud.google.com/go/video v1.19.0/go.mod h1:9qmqPqw/Ib2tLqaeHgtakU+l5TcJxCJbhFXM7UJjVzU= +cloud.google.com/go/videointelligence v1.11.1/go.mod h1:76xn/8InyQHarjTWsBR058SmlPCwQjgcvoW0aZykOvo= +cloud.google.com/go/vision/v2 v2.7.2/go.mod h1:jKa8oSYBWhYiXarHPvP4USxYANYUEdEsQrloLjrSwJU= +cloud.google.com/go/vmmigration v1.7.1/go.mod h1:WD+5z7a/IpZ5bKK//YmT9E047AD+rjycCAvyMxGJbro= +cloud.google.com/go/vmwareengine v1.0.0/go.mod h1:Px64x+BvjPZwWuc4HdmVhoygcXqEkGHXoa7uyfTgSI0= +cloud.google.com/go/vpcaccess v1.7.1/go.mod h1:FogoD46/ZU+JUBX9D606X21EnxiszYi2tArQwLY4SXs= +cloud.google.com/go/webrisk v1.9.1/go.mod h1:4GCmXKcOa2BZcZPn6DCEvE7HypmEJcJkr4mtM+sqYPc= +cloud.google.com/go/websecurityscanner v1.6.1/go.mod h1:Njgaw3rttgRHXzwCB8kgCYqv5/rGpFCsBOvPbYgszpg= +cloud.google.com/go/workflows v1.11.1/go.mod h1:Z+t10G1wF7h8LgdY/EmRcQY8ptBD/nvofaL6FqlET6g= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= +github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM= +github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/sagikazarmark/crypt v0.15.0/go.mod h1:5rwNNax6Mlk9sZ40AcyVtiEw24Z4J04cfSioF2COKmc= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/v2 v2.305.9/go.mod h1:0NBdNx9wbxtEQLwAQtrDHwx58m02vXpDcgSYI2seohQ= +go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:qDbnxtViX5J6CvFbxeNUSzKgVlDLJ/6L+caxye9+Flo= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/package-lock.json b/package-lock.json index 7062478..934a1a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "react": "^19.0.1", "react-dom": "^19.0.1", "react-leaflet": "^5.0.0", + "react-router-dom": "^7.2.0", "recharts": "^3.8.1", "vite": "^6.2.3" }, @@ -3722,6 +3723,57 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/recharts": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", @@ -3922,6 +3974,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", diff --git a/package.json b/package.json index a327285..d730ec6 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react": "^19.0.1", "react-dom": "^19.0.1", "react-leaflet": "^5.0.0", + "react-router-dom": "^7.2.0", "recharts": "^3.8.1", "vite": "^6.2.3" }, diff --git a/scratch/test.js b/scratch/test.js new file mode 100644 index 0000000..33c9a29 --- /dev/null +++ b/scratch/test.js @@ -0,0 +1,12 @@ +const https = require('https'); + +const req = https.request('https://fiesta.nearle.app/live/api/v1/web/users/getallusers?tenantid=1087&pagesize=500', { + method: 'GET' +}, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + console.log(data); + }); +}); +req.end(); diff --git a/src/App.tsx b/src/App.tsx index 91bc348..1c1a860 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ */ import React, { useState, useEffect } from 'react'; +import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { Network, Truck, @@ -29,7 +30,8 @@ import { Layers, Store, Settings, - LayoutDashboard + LayoutDashboard, + Users } from 'lucide-react'; import { MainSection } from './types'; @@ -37,9 +39,10 @@ import { useFiestaTenantLocations, useFiestaLocationSummary, useFiestaUpdateLocation, + useFiestaCreateLocation, useFiestaOrderSummary, } from './services/fiestaQueries'; -import { FIESTA_TENANT_ID, str as fstr } from './services/fiestaApi'; +import { FIESTA_TENANT_ID, str as fstr, num as fnum } from './services/fiestaApi'; import Sidebar from './components/Sidebar'; import Header from './components/Header'; import DashboardView from './components/DashboardView'; @@ -97,13 +100,14 @@ export default function App() { }; // Navigation indicators states - const [currentSection, setCurrentSection] = useState('dashboard'); - const [selectedStore, setSelectedStore] = useState<{ locationid?: number; name: string; zone: string; deliveries: number; sales: string; orders?: number; staff: string; color: string; status: string } | null>(null); + const location = useLocation(); + const navigate = useNavigate(); - const handleSetSection = (sec: MainSection) => { - setCurrentSection(sec); - setSelectedStore(null); - }; + // Derive current section from URL path, default to 'dashboard' + const pathParts = location.pathname.split('/'); + const currentSection = (pathParts.includes('admin') ? pathParts[pathParts.length - 1] : 'dashboard') as MainSection; + + const [selectedStore, setSelectedStore] = useState<{ locationid?: number; name: string; zone: string; deliveries: number; sales: string; orders?: number; staff: string; color: string; status: string } | null>(null); const [isCoimbatoreView, setIsCoimbatoreView] = useState(false); const [searchQuery, setSearchQuery] = useState(''); @@ -474,31 +478,7 @@ export default function App() { - {/* Live Sparkline Trend Histogram */} -
-
- Speed Index (Live Feed) - - Live - -
-
- {[30, 48, 25, 62, 54, 75, 42, 80, Math.min(95, Math.max(15, st.deliveries * 1.8))].map((val, idx) => ( -
-
40 - ? 'bg-rose-500/80 group-hover/bar:bg-rose-500' - : 'bg-[#662582]/70 group-hover/bar:bg-[#662582]' - : 'bg-amber-500/70 group-hover/bar:bg-amber-500' - }`} - /> -
- ))} -
-
+ {/* Lead Manager Profile block */}
@@ -567,22 +547,28 @@ export default function App() { }; // ── Auth gate ────────────────────────────────────────────────────────────── - // Signed out → login screen. User role → their single allocated store console - // (scoped by applocationid). Only the admin role reaches the full operations - // dashboard with the all-stores registry below. if (authRole === null) { - return ; - } - if (authRole === 'user' && authUser) { - return ; + return ( + + } /> + } /> + + ); } - return ( -
+ if (authRole === 'user' && authUser) { + return ( + + } /> + } /> + + ); + } + + const AdminConsole = ( +
{/* Navbar segment */}
setSidebarOpen((prev) => !prev)} isSidebarOpen={sidebarOpen} @@ -610,60 +596,69 @@ export default function App() { /> {/* Main Container workspace layout splits */} -
+
{/* Interactive Left Rail */} setSidebarOpen(false)} /> {/* Main core pages payload area */}
- {/* Nav content routing */} - {currentSection === 'dashboard' && ( - selectedStore ? ( - renderStoresSection() - ) : ( -
- -
- {renderStoresSection()} + + } /> + + +
+ {renderStoresSection()} +
-
- ) - )} - - {currentSection === 'inventory' && ( - - )} - - {currentSection === 'reports' && ( - - )} - - - {/* Handle alternative sections: Settings */} - {['settings'].includes(currentSection) && - renderSecondarySection() - } + ) + } /> + + + } /> + + + } /> + + + } /> + +
+
+ ); + + return ( + <> + + + } /> + {/* CALENDAR SCHEDULER DIALOG MODAL */} {showCalendarModal && ( @@ -798,6 +793,6 @@ export default function App() { )} -
+ ); } diff --git a/src/components/AdminConsole.tsx b/src/components/AdminConsole.tsx index 996e559..5f126b4 100644 --- a/src/components/AdminConsole.tsx +++ b/src/components/AdminConsole.tsx @@ -151,6 +151,10 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr const res = await createLocationMut.mutateAsync({ ...storeForm, status: 'Active', + moduleid: 2, + applocationid: 1, + partnerid: 1, + roleid: 4, }); setStoreSuccess(res); alert('Store Location created successfully!'); diff --git a/src/components/AdminCustomersView.tsx b/src/components/AdminCustomersView.tsx new file mode 100644 index 0000000..d2f04a4 --- /dev/null +++ b/src/components/AdminCustomersView.tsx @@ -0,0 +1,199 @@ +/** + * @license + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { Users, Search, Phone, Mail, ChevronRight, Award, MapPin } from 'lucide-react'; +import { useFiestaTenantCustomers, useFiestaTenantLocations } from '../services/fiestaQueries'; +import { num as fnum, str as fstr } from '../services/fiestaApi'; + +interface AdminCustomersViewProps { + tenantId: number; +} + +export default function AdminCustomersView({ tenantId }: AdminCustomersViewProps) { + const [customerSearch, setCustomerSearch] = useState(''); + + // Fetch all customers for the tenant by passing locationid: 0 + const customersQ = useFiestaTenantCustomers({ + tenantid: tenantId, + locationid: 0, + pagesize: 500, + }); + + const locationsQ = useFiestaTenantLocations(tenantId); + const locations = locationsQ.data ?? []; + + const getMergedCustomers = () => { + const rawCustomers = customersQ.data ?? []; + return rawCustomers.map((c: any) => { + const id = + fnum(c.customerid) || + fnum(c.customerheaderid) || + fnum(c.userid) || + fnum(c.id) || + null; + const spent = fnum(c.totalspent); + + const locId = fnum(c.locationid) || fnum(c.applocationid); + const loc = locations.find((l: any) => fnum(l.locationid) === locId || fnum(l.applocationid) === locId); + const storeName = loc ? fstr(loc.locationname) : (fstr(c.locationname) || 'Central'); + + return { + id, + name: fstr(c.fullname) || `${fstr(c.firstname)} ${fstr(c.lastname)}`.trim() || 'Customer', + phone: fstr(c.contactno) || '—', + email: fstr(c.email), + address: fstr(c.address) || 'Coimbatore', + ordersCount: Number(c.orderscount) || 0, + totalSpent: spent > 0 ? `₹${spent.toLocaleString('en-IN')}` : '—', + storeName, + }; + }); + }; + + const customersList = getMergedCustomers().filter(c => + c.name.toLowerCase().includes(customerSearch.toLowerCase()) || + c.phone.includes(customerSearch) || + c.address.toLowerCase().includes(customerSearch.toLowerCase()) || + c.storeName.toLowerCase().includes(customerSearch.toLowerCase()) + ); + + // Helper for jewel-tone avatars + const toneFor = (str: string) => { + let hash = 0; + for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash); + const hues = [ + { soft: '#f3e8ff', fg: '#6b21a8' }, // purple + { soft: '#e0e7ff', fg: '#3730a3' }, // indigo + { soft: '#fce7f3', fg: '#9d174d' }, // pink + { soft: '#ffedd5', fg: '#9a3412' }, // orange + { soft: '#dcfce7', fg: '#166534' }, // green + { soft: '#cffafe', fg: '#155e75' }, // cyan + ]; + return hues[Math.abs(hash) % hues.length]; + }; + + const initialsOf = (name: string) => { + const p = name.split(' ').filter(Boolean); + return ((p[0]?.[0] || '') + (p.length > 1 ? p[p.length - 1][0] : '')).toUpperCase(); + }; + + const localityOf = (addr: string) => { + if (!addr) return ''; + const p = addr.split(','); + return p.length > 1 ? p[p.length - 2].trim() : p[0].trim(); + }; + + return ( +
+
+
+

+ + Customers +

+
+ +
+ + setCustomerSearch(e.target.value)} + className="w-full pl-9 pr-3 py-2.5 border border-[#662582]/20 rounded-xl text-[13px] text-[#0f172a] placeholder:text-zinc-400 outline-none bg-white focus:border-[#662582] focus:ring-4 focus:ring-[#662582]/10 transition-all shadow-sm" + /> +
+
+ + {customersQ.isLoading ? ( +
+
+ + +
+

Loading Customers...

+
+ ) : customersList.length === 0 ? ( +
+ +

No customers found

+

+ {customerSearch ? 'Nothing matches your search criteria. Try a different term.' : 'There are no customers registered in the tenant yet.'} +

+ {customerSearch && ( + + )} +
+ ) : ( +
+ {customersList.map((c: any, idx: number) => { + const tone = toneFor(c.name || `c${idx}`); + const locality = localityOf(c.address); + return ( +
+
+
+ + {initialsOf(c.name)} + +
+

{c.name}

+ {locality && ( +

+ {locality} +

+ )} +
+
+ {c.ordersCount > 5 && ( + + Loyal + + )} +
+ +
+
+ Orders + {c.ordersCount} +
+
+ Gross Spend + {c.totalSpent} +
+
+ +
+
+
+ {c.phone} +
+ {c.email && c.email !== '—' && ( +
+
+ {c.email} +
+ )} +
+ +
+ + {c.storeName} + +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx new file mode 100644 index 0000000..ecff1ca --- /dev/null +++ b/src/components/ConfirmModal.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { AlertTriangle, X } from 'lucide-react'; + +interface ConfirmModalProps { + isOpen: boolean; + title: string; + message: React.ReactNode; + confirmText?: string; + cancelText?: string; + onConfirm: () => void; + onCancel: () => void; + isDestructive?: boolean; + isLoading?: boolean; +} + +export function ConfirmModal({ + isOpen, + title, + message, + confirmText = 'Confirm', + cancelText = 'Cancel', + onConfirm, + onCancel, + isDestructive = false, + isLoading = false, +}: ConfirmModalProps) { + if (!isOpen) return null; + + return ( +
{ if (e.target === e.currentTarget && !isLoading) onCancel(); }} + > +
e.stopPropagation()} + > +
+
+ {isDestructive && } +

{title}

+
+ +
+ +
+
+ {message} +
+
+ +
+ + +
+
+
+ ); +} diff --git a/src/components/DeliveryReportsView.tsx b/src/components/DeliveryReportsView.tsx index d9e8cf7..05e9db1 100644 --- a/src/components/DeliveryReportsView.tsx +++ b/src/components/DeliveryReportsView.tsx @@ -490,7 +490,7 @@ function OrdersSummaryReport({ tenantId, locationid, fromdate, todate }: { tenan // ── Riders Summary (per rider) ─────────────────────────────────────────────────── function RidersSummaryReport({ fromdate, todate, tenantId, locationid }: { fromdate: string; todate: string; tenantId: number; locationid?: number }) { - const q = useFiestaFleetSummary({ tenantid: tenantId, fromdate, todate, applocationid: locationid }); + const q = useFiestaFleetSummary({ tenantid: tenantId, fromdate, todate }); const rows = q.data ?? []; const mapped = rows.map((r) => ({ name: fstr(r.fullname) || `${fstr(r.firstname)} ${fstr(r.lastname)}`.trim() || fstr(r.username) || `Rider ${fstr(r.userid)}`, diff --git a/src/components/InventoryView.tsx b/src/components/InventoryView.tsx index 949afee..1cbeef2 100644 --- a/src/components/InventoryView.tsx +++ b/src/components/InventoryView.tsx @@ -42,6 +42,7 @@ import { useFiestaProductCategories, useFiestaMasterCatalog, useFiestaUpdateStockRequest, + useFiestaGetStockRequests, } from '../services/fiestaQueries'; import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi'; import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers'; @@ -51,7 +52,6 @@ import AwaitingApi from './AwaitingApi'; import { SlideDrawer, Skeleton, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND, tint, edge, StatusChip } from './consoleUi'; import FMCGHoverOverlay from './FMCGHoverOverlay'; import { useCompare } from '../contexts/CompareContext'; -import TrialBatchDrawer, { TrialProduct } from './TrialBatchDrawer'; const MOCK_GLOBAL_CATALOG: ProductMatrixItem[] = [ { @@ -122,7 +122,7 @@ export default function InventoryView({ tenantId = FIESTA_TENANT_ID }: InventoryViewProps) { const { selectedProducts, toggleProduct, setIsComparing, clearSelection, setHideCompareBar } = useCompare(); - const [trialProducts, setTrialProducts] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); // ── Live stock across every outlet (Fiesta) ─────────────────────────────── // This page is the admin's command surface. The GLOBAL CATALOG is the deduped @@ -185,99 +185,70 @@ export default function InventoryView({ const [requestStoreFilter, setRequestStoreFilter] = useState('All Stores'); const [storeRequests, setStoreRequests] = useState<{ locationid: number, locationname: string, picks: Record }[]>([]); + const stockRequestsQ = useFiestaGetStockRequests({ tenantid: tenantId, pagesize: 1000 }); useEffect(() => { - if (activeTab === 'requests') { - const reqs: any[] = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && key.startsWith('nearledaily.catalogue.request.')) { - const locIdStr = key.split('.').pop(); - if (locIdStr && locIdStr !== 'na') { - const locId = parseInt(locIdStr); - const raw = localStorage.getItem(key); - if (raw) { - try { - const parsed = JSON.parse(raw); - const picks: Record = {}; - for (const [k, v] of Object.entries(parsed)) { - if (typeof v === 'number') picks[k] = { qty: v, status: 'Pending', requestedAt: new Date().toISOString() }; - else { - picks[k] = v; - if (picks[k].status === 'Approve') picks[k].status = 'Approved'; - if (picks[k].status === 'Reject') picks[k].status = 'Rejected'; - } - } - if (Object.keys(picks).length > 0) { - const loc = locations.find(l => l.locationid === locId); - reqs.push({ - locationid: locId, - locationname: loc ? loc.locationname : `Outlet #${locId}`, - picks - }); - } - } catch (e) {} - } - } + if (activeTab === 'requests' && stockRequestsQ.data) { + const reqsMap: Record = {}; + stockRequestsQ.data.forEach((req: any) => { + if (!reqsMap[req.locationid]) { + const loc = locations.find(l => l.locationid === req.locationid); + reqsMap[req.locationid] = { + locationid: req.locationid, + locationname: loc ? loc.locationname : `Outlet #${req.locationid}`, + picks: {} + }; } - } - setStoreRequests(reqs); + if (!reqsMap[req.locationid].picks[req.productid]) { + reqsMap[req.locationid].picks[req.productid] = { + qty: req.qty, + status: req.status, + requestedAt: req.created, + resolvedAt: req.updated, + productname: req.productname, + productimage: req.productimage, + requestid: req.requestid + }; + } + }); + setStoreRequests(Object.values(reqsMap).filter(r => Object.keys(r.picks).length > 0)); } - }, [activeTab, locations]); + }, [activeTab, locations, stockRequestsQ.data]); const updateStockRequestMutation = useFiestaUpdateStockRequest(); const updateProductRequestStatus = (locationid: number, productid: string, status: 'Approved' | 'Rejected' | 'Pending') => { - // Optimistic API update + // Find the requestid from the current state + let requestid = 0; + storeRequests.forEach(r => { + if (r.locationid === locationid && r.picks[productid]) { + requestid = r.picks[productid].requestid; + } + }); + + if (!requestid) return; + updateStockRequestMutation.mutate({ tenantid: tenantId, locationid, productid: Number(productid), + requestid, status }); - const key = `nearledaily.catalogue.request.${locationid}`; - const raw = localStorage.getItem(key); - if (raw) { - try { - const picks = JSON.parse(raw); - if (picks[productid] != null) { - const existing = typeof picks[productid] === 'number' - ? { qty: picks[productid], status: 'Pending', requestedAt: new Date().toISOString() } - : picks[productid]; - - picks[productid] = { ...existing, status, resolvedAt: status === 'Pending' ? undefined : new Date().toISOString() }; - localStorage.setItem(key, JSON.stringify(picks)); - - if (status === 'Approved') { - const prod = products.find(p => String(p.id) === String(productid)) || MOCK_GLOBAL_CATALOG.find(p => String(p.id) === String(productid)); - if (prod) { - if (storeCat.has(productid)) { - storeCat.setQty(productid, storeCat.getQty(productid) + existing.qty); - } else { - storeCat.add({ - productid: String(prod.id), - name: prod.name, - image: prod.image, - category: prod.category, - sku: prod.sku, - price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 10, - unit: prod.exposure || 'Piece', - qty: existing.qty - }); - } - } - } - - setStoreRequests(prev => prev.map(r => { - if (r.locationid === locationid) { - return { ...r, picks: { ...r.picks, [productid]: picks[productid] } }; - } - return r; - })); - } - } catch (e) {} - } + // Optimistic UI update + setStoreRequests(prev => prev.map(r => { + if (r.locationid === locationid && r.picks[productid]) { + return { + ...r, + picks: { + ...r.picks, + [productid]: { ...r.picks[productid], status, resolvedAt: new Date().toISOString() } + } + }; + } + return r; + })); }; // Hide compare bar when not in Global Catalogue @@ -1020,22 +991,6 @@ export default function InventoryView({ Import to Catalogue )} -
@@ -1185,22 +1140,6 @@ export default function InventoryView({ Import to Catalogue )} - @@ -1263,7 +1202,7 @@ export default function InventoryView({

No Pending Requests

-

+

You're all caught up! All store stock requests have been processed and there is currently no pending action required.

@@ -1505,22 +1444,6 @@ export default function InventoryView({ > Import Local - )} @@ -1661,23 +1584,6 @@ export default function InventoryView({ })()} - 0} - onClose={() => setTrialProducts([])} - product={trialProducts[0] || null} - tenantId={tenantId} - onSuccess={(tp) => { - const globalProd = MOCK_GLOBAL_CATALOG.find(p => p.id === tp.id); - if (globalProd) { - setProducts(prev => { - if (prev.some(p => p.id === globalProd.id)) return prev; - return [{ ...globalProd, isSample: true, isNew: true }, ...prev]; - }); - setActiveTab('catalog'); - } - }} - /> - {/* ── Floating Import Button ── */} {activeTab === 'import_branding' && selectedProducts.length > 0 && ( - +
+
+ + +
+ + {/* Global Store Filter */} + {activeTab === 'sales_revenue' && ( +
+ + +
+ )}
{activeTab === 'overview' ? ( @@ -1096,7 +1127,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba )} ) : ( - + )} ); diff --git a/src/components/SalesRevenueReport.tsx b/src/components/SalesRevenueReport.tsx index 1b64055..94f8c2b 100644 --- a/src/components/SalesRevenueReport.tsx +++ b/src/components/SalesRevenueReport.tsx @@ -17,55 +17,11 @@ import { TrendingDown } from 'lucide-react'; import { - useFiestaAllOrders + useFiestaAllOrders, + useFiestaSalesSummary } from '../services/fiestaQueries'; import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi'; -const generateMockOrders = (from: string, to: string, locationid?: number): Row[] => { - const fromTime = new Date(from).getTime(); - const toTime = new Date(to).getTime(); - const mockOrders: Row[] = []; - - const locations = [ - { id: 1166, name: 'Main Hub' }, - { id: 1167, name: 'Downtown Branch' }, - { id: 1168, name: 'Westside Market' } - ]; - const paymentModes = ['CASH', 'UPI', 'UPI', 'CARD']; - const statuses = ['delivered', 'delivered', 'delivered', 'completed', 'cancelled']; - const customers = ['John Doe', 'Jane Smith', 'Acme Corp', 'Bob Johnson', 'Local Cafe', 'John Doe']; - - // Seeded random for stable mock data across renders - let seed = 12345; - const random = () => { - seed = (seed * 9301 + 49297) % 233280; - return seed / 233280; - }; - - for (let i = 0; i < 65; i++) { - const loc = locationid - ? locations.find(l => l.id === locationid) || { id: locationid, name: `Store ${locationid}` } - : locations[Math.floor(random() * locations.length)]; - const time = new Date(fromTime + random() * (toTime - fromTime + 86400000)); - - // Force peaks around lunch and dinner - if (random() > 0.5) time.setHours(11 + Math.floor(random() * 3)); // 11am-2pm - else time.setHours(17 + Math.floor(random() * 4)); // 5pm-9pm - - mockOrders.push({ - orderid: `ORD-MOCK-${8400 + i}`, - orderdate: time.toISOString(), - orderstatus: statuses[Math.floor(random() * statuses.length)], - ordervalue: Math.floor(random() * 2500) + 150, - quantity: Math.floor(random() * 8) + 1, - paymentmode: paymentModes[Math.floor(random() * paymentModes.length)], - locationid: loc.id, - applocation: loc.name, - deliverycustomer: customers[Math.floor(random() * customers.length)], - }); - } - return mockOrders; -}; // ... inside the component ... import { shortTime } from '../services/fiestaMappers'; @@ -108,12 +64,19 @@ export default function SalesRevenueReport({ const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom'; // ── Queries ────────────────────────────────────────────────────────────────── + const salesSummaryQ = useFiestaSalesSummary({ tenantid: tenantId, fromdate, todate, locationid }); const allOrdersQ = useFiestaAllOrders({ tenantid: tenantId, fromdate, todate, locationid }); - const mockData = useMemo(() => generateMockOrders(fromdate, todate, locationid), [fromdate, todate, locationid]); - const allRows = allOrdersQ.data && allOrdersQ.data.length > 0 ? allOrdersQ.data : mockData; + const allRows = allOrdersQ.data || []; // ── Calculations ───────────────────────────────────────────────────────────── - // Filter only delivered orders for actual sales/revenue calculation + const salesData = salesSummaryQ.data; + const totalRevenue = salesData?.totalRevenue ?? 0; + const totalOrders = salesData?.totalOrders ?? 0; + const aov = salesData?.averageOrderValue ?? 0; + const chartData = salesData?.chartData ?? []; + const backendTopLocations = salesData?.topLocations ?? []; + + // Filter only delivered orders for client-side stats (retention, hourly, etc.) const completedOrders = useMemo(() => { return allRows.filter((r) => { const s = fstr(r.orderstatus).toLowerCase(); @@ -121,52 +84,6 @@ export default function SalesRevenueReport({ }); }, [allRows]); - const { totalRevenue, totalOrders } = useMemo(() => { - let rev = 0; - let ords = 0; - for (const r of completedOrders) { - if (locationid) { - const rLoc = fnum(r.locationid); - const rApp = fnum(r.applocationid); - if (rLoc !== locationid && rApp !== locationid) continue; - } - - const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); - rev += amt; - ords += 1; - } - return { totalRevenue: rev, totalOrders: ords }; - }, [completedOrders, locationid]); - - const aov = totalOrders > 0 ? totalRevenue / totalOrders : 0; - - // Chart data (Group by date) - const chartData = useMemo(() => { - const map = new Map(); - - for (const r of completedOrders) { - if (locationid) { - const rLoc = fnum(r.locationid); - const rApp = fnum(r.applocationid); - if (rLoc !== locationid && rApp !== locationid) continue; - } - - const dateVal = fstr(r.orderdate) || fstr(r.deliverydate); - if (!dateVal) continue; - - const key = dateVal.split('T')[0]; - const ex = map.get(key) || { date: key, revenue: 0, orders: 0 }; - - const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); - ex.revenue += amt; - ex.orders += 1; - - map.set(key, ex); - } - - return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date)); - }, [completedOrders, locationid]); - const formatLabel = (key: string) => { const d = new Date(key); const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; @@ -234,19 +151,9 @@ export default function SalesRevenueReport({ }, [completedOrders, locationid]); // 4. Top Locations Leaderboard (Admin Only) - const topLocations = useMemo(() => { - if (locationid) return []; // Only for admin - const lmap = new Map(); - for (const r of completedOrders) { - const locStr = fstr(r.applocation) || fstr(r.locationname) || `Location ${fstr(r.locationid) || fstr(r.applocationid)}`; - const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); - lmap.set(locStr, (lmap.get(locStr) || 0) + amt); - } - return Array.from(lmap.entries()) - .map(([name, revenue]) => ({ name, revenue })) - .sort((a, b) => b.revenue - a.revenue) - .slice(0, 5); // Top 5 - }, [completedOrders, locationid]); + const topLocations = backendTopLocations.length > 0 + ? backendTopLocations.map(l => ({ name: l.locationname, revenue: l.revenue })) + : []; const kpis = [ { label: 'Total Sales', value: totalOrders.toLocaleString('en-IN'), color: '#6366f1', icon: , badge: 'Completed orders' }, diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index be5b4e7..7201425 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -17,12 +17,13 @@ import { Plus, Bike } from 'lucide-react'; -import { useFiestaAllTenants, useFiestaTenantLocations } from '../services/fiestaQueries'; +import { useFiestaAllTenants, useFiestaTenantLocations, useFiestaUpdateLocation, useFiestaDeleteLocation } from '../services/fiestaQueries'; import { useAppRoles } from '../services/queries'; import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi'; import UsersPanel from './UsersPanel'; import AwaitingApi from './AwaitingApi'; import AdminConsole from './AdminConsole'; +import { ConfirmModal } from './ConfirmModal'; import type { AuthUser } from '../services/auth'; type TabKey = 'profile' | 'outlets' | 'users'; @@ -143,6 +144,50 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett // (see [R6]) so they are not persisted; the operational controls that would // need persistence show an AwaitingApi notice instead of saving silently. const [form, setForm] = useState({ ...DEFAULTS }); + + const updateLocationMut = useFiestaUpdateLocation(); + const deleteLocationMut = useFiestaDeleteLocation(); + const [editingStore, setEditingStore] = useState(null); + const [showEditStoreModal, setShowEditStoreModal] = useState(false); + const [confirmModalState, setConfirmModalState] = useState<{isOpen: boolean, loc: any}>({ isOpen: false, loc: null }); + + const handleUpdateStoreSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!editingStore) return; + try { + await updateLocationMut.mutateAsync({ + tenantid: Number(tenantId), + locationid: Number(editingStore.locationid), + locationname: editingStore.locationname, + suburb: editingStore.suburb, + city: editingStore.city, + contactno: editingStore.contactno, + status: editingStore.status, + }); + setShowEditStoreModal(false); + setEditingStore(null); + alert('Outlet updated successfully!'); + } catch (err: any) { + alert(err.message || 'Failed to update outlet.'); + } + }; + + const handleDeleteStore = async (loc: any) => { + setConfirmModalState({ isOpen: true, loc }); + }; + + const confirmDeleteOutlet = async () => { + const loc = confirmModalState.loc; + if (!loc) return; + try { + await deleteLocationMut.mutateAsync({ tenantid: tenantId, locationid: loc.locationid }); + setConfirmModalState({ isOpen: false, loc: null }); + } catch (err: any) { + alert(err.message || 'Failed to delete outlet.'); + setConfirmModalState({ isOpen: false, loc: null }); + } + }; + const [showStoreOnboarding, setShowStoreOnboarding] = useState(false); useEffect(() => { @@ -210,11 +255,10 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett }, [rolesQ.data]); return ( -
+
{/* Header */} -
- -
+
+
{tenantsQ.isLoading ? ( Loading store profile… @@ -420,7 +464,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
{/* Header: Outlet name & status */}
-
+
@@ -432,30 +476,43 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett

- - {loc.status || '—'} - +
+ + {loc.status || '—'} + + + +
{/* Outlet Details Grid */} -
+
Delivery Range

{loc.deliveryradius ? `Up to ${loc.deliveryradius / 1000} km` : '—'}

-
- Delivery Speed -

- {loc.deliverymins ? `${loc.deliverymins} mins avg` : '—'} -

-
-
+
Opening Hours

@@ -483,6 +540,138 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett

+ + {/* EDIT STORE MODAL */} + {showEditStoreModal && editingStore && ( +
{ if (e.target === e.currentTarget) setShowEditStoreModal(false); }} + > +
e.stopPropagation()}> +
+

+ + Edit Store Outlet Location +

+ +
+ +
+
+
+ +
+
Update Outlet Profile
+

+ Adjust the operational metadata for this node. The outlet will instantly be updated across the delivery fleet system. +

+
+
+ +
+
+ + setEditingStore({ ...editingStore, locationname: e.target.value })} + className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" + required + /> +
+ +
+ + setEditingStore({ ...editingStore, suburb: e.target.value })} + className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" + required + /> +
+ +
+ + setEditingStore({ ...editingStore, city: e.target.value })} + className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" + /> +
+ +
+ + setEditingStore({ ...editingStore, contactno: e.target.value })} + className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" + required + /> +
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ )} + + + Are you sure you want to delete {confirmModalState.loc?.locationname}? This action cannot be undone. + + } + confirmText="Delete Outlet" + onConfirm={confirmDeleteOutlet} + onCancel={() => setConfirmModalState({ isOpen: false, loc: null })} + isDestructive={true} + isLoading={deleteLocationMut.isPending} + />
); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 3183b0d..5850c08 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -10,27 +10,28 @@ import { Layers, Settings, TrendingUp, - ShieldAlert + ShieldAlert, + Users } from 'lucide-react'; +import { NavLink, useLocation } from 'react-router-dom'; import { MainSection } from '../types'; interface SidebarProps { - currentSection: MainSection; - setCurrentSection: (section: MainSection) => void; isCoimbatoreView: boolean; setIsCoimbatoreView: (val: boolean) => void; isOpen: boolean; isAdmin?: boolean; + onClose: () => void; } export default function Sidebar({ - currentSection, - setCurrentSection, isCoimbatoreView, setIsCoimbatoreView, isOpen, - isAdmin + isAdmin, + onClose }: SidebarProps) { + const location = useLocation(); // Navigation elements const navItems = [ { id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard }, @@ -40,36 +41,48 @@ export default function Sidebar({ ]; return ( - + <> + {/* Mobile Overlay */} + {isOpen && ( +
+ )} + + + ); } - diff --git a/src/components/StoreCatalogView.tsx b/src/components/StoreCatalogView.tsx index 6648c50..8566bf5 100644 --- a/src/components/StoreCatalogView.tsx +++ b/src/components/StoreCatalogView.tsx @@ -21,7 +21,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity, Info, Inbox } from 'lucide-react'; -import { useFiestaStockStatement, useFiestaCreateStockRequest, FIESTA_TENANT_ID } from '../services/fiestaQueries'; +import { useFiestaStockStatement, useFiestaCreateStockRequest, useFiestaGetStockRequests, useFiestaUpdateStockRequest, FIESTA_TENANT_ID } from '../services/fiestaQueries'; import { num as fnum, str as fstr, type Row, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi'; import { categoryName } from '../services/fiestaMappers'; import { useStoreCatalogue } from '../services/storeCatalogue'; @@ -82,54 +82,29 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', [storeCat.items], ); - // The user's picks: productid → request data. Persisted per store. - const storageKey = `nearledaily.catalogue.request.${locationid ?? 'na'}`; - const [picks, setPicks] = useState>(() => { - try { - const raw = localStorage.getItem(storageKey); - if (!raw) return {}; - const parsed = JSON.parse(raw); - // Migrate old format (Record) to new format - const migrated: Record = {}; - for (const [k, v] of Object.entries(parsed)) { - if (typeof v === 'number') { - migrated[k] = { qty: v, status: 'Pending', requestedAt: new Date().toISOString() }; - } else { - migrated[k] = v; - if (migrated[k].status === 'Approve') migrated[k].status = 'Approved'; - if (migrated[k].status === 'Reject') migrated[k].status = 'Rejected'; + const stockRequestsQ = useFiestaGetStockRequests({ tenantid, locationid: locationid ?? 0, pagesize: 500 }); + const [picks, setPicks] = useState>({}); + + useEffect(() => { + if (stockRequestsQ.data) { + const map: Record = {}; + stockRequestsQ.data.forEach((req: any) => { + if (!map[req.productid]) { + map[req.productid] = { + qty: req.qty, + status: req.status, + requestedAt: req.created, + resolvedAt: req.updated, + requestid: req.requestid + }; } - } - return migrated; - } catch { - return {}; + }); + setPicks(map); } - }); - - // Listen for storage events so approvals from Admin update dynamically - useEffect(() => { - const handler = (e: StorageEvent) => { - if (e.key === storageKey) { - if (e.newValue) { - const parsed = JSON.parse(e.newValue); - for (const key of Object.keys(parsed)) { - if (parsed[key].status === 'Approve') parsed[key].status = 'Approved'; - if (parsed[key].status === 'Reject') parsed[key].status = 'Rejected'; - } - setPicks(parsed); - } - else setPicks({}); - } - }; - window.addEventListener('storage', handler); - return () => window.removeEventListener('storage', handler); - }, [storageKey]); - - useEffect(() => { - localStorage.setItem(storageKey, JSON.stringify(picks)); - }, [picks, storageKey]); + }, [stockRequestsQ.data]); const createRequestMutation = useFiestaCreateStockRequest(); + const updateRequestMutation = useFiestaUpdateStockRequest(); const togglePick = (id: string) => { setNotice(false); @@ -271,14 +246,14 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', // The request is saved to localStorage automatically via the useEffect on `picks`. return ( -
+
{/* Tabs */}
-
+
{/* Sticky Sidebar Filter */} {view !== 'requests' && (
@@ -439,49 +414,53 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', sub="Try a different keyword or clear the filters to see the full catalogue." /> ) : ( -
+
{filtered.map((p) => { const stocked = inStore.has(p.id); const pick = picks[p.id]; const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Approved'; return ( -
setSelectedProduct(p)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer overflow-hidden"> +
setSelectedProduct(p)} className="cursor-pointer bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden"> {/* Image Bento Block */} -
- {p.name} -
+
+ {p.name} +
{stocked && ( -
- - In Store +
+ + In Store
)} -
- +
+ {p.category.split(' / ')[0]}
-
{/* Details Bento Block */} -
+
-
-

{p.name}

-

{p.sku}

+
+

{p.name}

+

{p.sku}

-
+
- Price - {p.price > 0 ? `₹${p.price.toLocaleString('en-IN')}` : '—'} + Price + {p.price > 0 ? `₹${p.price.toLocaleString('en-IN')}` : '—'}
- Unit - {p.unit || 'Pc'} + Unit + {p.unit || 'Pc'}
@@ -545,7 +524,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', - {['Requested At', 'Product', 'Qty', 'Status', 'Resolved At'].map((h, i) => ( + {['Requested At', 'Product', 'Qty', 'Status', 'Resolved At', ''].map((h, i) => ( ))} @@ -612,6 +591,25 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', {data.resolvedAt ? new Date(data.resolvedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'} + ); })} @@ -642,38 +640,42 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', } /> ) : ( -
+
{finalFilteredInventory.map((it, i) => ( -
setSelectedProduct(it)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer overflow-hidden"> +
setSelectedProduct(it)} className="cursor-pointer bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden"> {/* Image Bento Block */} -
- {it.name} -
+
+ {it.name} +
-
+
- {it.label} + {it.label}
-
- +
+ {it.category.split(' / ')[0]}
-
{/* Details Bento Block */} -
+
-
-

{it.name}

-

{it.sku}

+
+

{it.name}

+

{it.sku}

-
- Live Stock - {(it.closing ?? 0).toLocaleString('en-IN')} +
+ Live Stock + {(it.closing ?? 0).toLocaleString('en-IN')}
diff --git a/src/components/StoreDetailView.tsx b/src/components/StoreDetailView.tsx index 032133c..6f8118d 100644 --- a/src/components/StoreDetailView.tsx +++ b/src/components/StoreDetailView.tsx @@ -964,12 +964,10 @@ export default function StoreDetailView({ store, onBack, canManage = true, only, {customersList.length === 0 ? (
-
-

No customers yet

-

- {customerSearch ? 'Nothing matches your search criteria.' : 'Customers will appear here once they place their first order.'} -

-
+

No customers yet

+

+ {customerSearch ? 'Nothing matches your search criteria.' : 'Customers will appear here once they place their first order.'} +

{customerSearch && ( )} diff --git a/src/components/UserStorePage.tsx b/src/components/UserStorePage.tsx index 02831aa..382d987 100644 --- a/src/components/UserStorePage.tsx +++ b/src/components/UserStorePage.tsx @@ -5,6 +5,7 @@ import React, { useState } from 'react'; import { createPortal } from 'react-dom'; +import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { AlertTriangle, LayoutDashboard, @@ -14,7 +15,7 @@ import { ShieldCheck, ShoppingBag, Truck, - Route, + Route as RouteIcon, ClipboardList, Layers, Users, @@ -49,9 +50,8 @@ interface UserStorePageProps { const NAV_ITEMS: UserNavItem[] = [ { id: 'console', label: 'Store Console', icon: LayoutDashboard }, { id: 'inventory', label: 'Products', icon: Layers }, - { id: 'pos', label: 'POS Terminal', icon: ShoppingBag }, { id: 'customers', label: 'Customers', icon: Users }, - { id: 'dispatch', label: 'Dispatch', icon: Route }, + { id: 'dispatch', label: 'Dispatch', icon: RouteIcon }, { id: 'reports', label: 'Reports', icon: ClipboardList }, ]; @@ -65,8 +65,13 @@ type StoreShape = React.ComponentProps['store']; */ export default function UserStorePage({ onLogout, user }: UserStorePageProps) { const [sidebarOpen, setSidebarOpen] = useState(true); - const [activeSection, setActiveSection] = useState('console'); const [showQrModal, setShowQrModal] = useState(false); + const location = useLocation(); + const navigate = useNavigate(); + + // Extract the current section from the URL for Header context + const pathParts = location.pathname.split('/'); + const activeSection = pathParts[pathParts.length - 1] || 'console'; // Scope every query to the signed-in merchant's tenant; the shared constant is // only a fallback for legacy sessions whose record predates tenantid capture. @@ -191,26 +196,6 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) { const renderSection = () => { if (activeSection === 'account') return renderAccount(); - // Logistics console — scoped to this user's store. These views own their - // loading/error states, so they don't need the store-console load gating below. - if (activeSection === 'pos') return ; - if (activeSection === 'dispatch') return ; - if (activeSection === 'reports') return ; - // Inventory & Catalog is its own page: the manager-curated catalog the user - // stocks from (the catalog query is tenant-level, so it doesn't need the store - // gating below — only "My Store Inventory" uses the resolved location id). - if (activeSection === 'inventory') return ; - - // The store console needs a resolved store, so gate it on the load state. - if (locationsQ.isLoading || locSummaryQ.isLoading) { - return ( -
-
- Loading your store… -
- ); - } - // Distinguish a real load failure from "no store assigned" — otherwise an // API outage would wrongly tell the user to contact their admin. if (locationsQ.isError || locSummaryQ.isError) { @@ -255,6 +240,26 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) { ); } + // Logistics console — scoped to this user's store. These views own their + // loading/error states, so they don't need the store-console load gating below. + if (activeSection === 'pos') return ; + if (activeSection === 'dispatch') return ; + if (activeSection === 'reports') return ; + // Inventory & Catalog is its own page: the manager-curated catalog the user + // stocks from (the catalog query is tenant-level, so it doesn't need the store + // gating below — only "My Store Inventory" uses the resolved location id). + if (activeSection === 'inventory') return ; + + // The store console needs a resolved store, so gate it on the load state. + if (locationsQ.isLoading || locSummaryQ.isLoading) { + return ( +
+
+ Loading your store… +
+ ); + } + // canManage=false hides write actions in the console. NOTE: this is a UI-only // restriction — the backend must also enforce role-based authorization on the // write endpoints, since a hidden button doesn't stop a direct API call. @@ -267,13 +272,13 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) { }; return ( -
+
setSidebarOpen((s) => !s)} onHelpClick={handleHelp} onLogoutClick={onLogout} - onAccountClick={() => setActiveSection('account')} + onAccountClick={() => navigate('/store/account')} onQrClick={() => setShowQrModal(true)} profile={profile} storeContext={{ @@ -295,35 +300,63 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) { : activeSection === 'customers' ? Users : activeSection === 'dispatch' - ? Route + ? RouteIcon : activeSection === 'reports' ? ClipboardList : undefined }} /> -
+
setSidebarOpen(false)} />
- {activeSection === 'dispatch' || activeSection === 'pos' ? ( -
{renderSection()}
- ) : ( -
- {renderSection()} -
- )} + + } /> + + {renderAccount()} +
+ } /> + + +
+ } /> + + +
+ } /> + + +
+ } /> + + +
+ } /> + + +
+ } /> + + +
+ } /> +
diff --git a/src/components/UserStoreSidebar.tsx b/src/components/UserStoreSidebar.tsx index b58e994..67b5e3a 100644 --- a/src/components/UserStoreSidebar.tsx +++ b/src/components/UserStoreSidebar.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { type LucideIcon } from 'lucide-react'; +import { NavLink } from 'react-router-dom'; /** One entry in the user store sidebar. Add to `UserStorePage`'s `NAV_ITEMS`. */ export interface UserNavItem { @@ -15,10 +16,9 @@ export interface UserNavItem { interface UserStoreSidebarProps { items: UserNavItem[]; - activeId: string; - onSelect: (id: string) => void; /** Collapsed → icon-only rail; expanded → labels visible. */ isOpen: boolean; + onClose: () => void; } /** @@ -26,24 +26,32 @@ interface UserStoreSidebarProps { * (fixed, sits under the fixed Header, collapses to an icon rail) but driven by * a generic items list so new user sections can be added without touching this. */ -export default function UserStoreSidebar({ items, activeId, onSelect, isOpen }: UserStoreSidebarProps) { +export default function UserStoreSidebar({ items, isOpen, onClose }: UserStoreSidebarProps) { return ( - + ); } diff --git a/src/main.tsx b/src/main.tsx index b288aec..e66cc21 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -33,11 +33,15 @@ const queryClient = new QueryClient({ }, }); +import { BrowserRouter } from 'react-router-dom'; + createRoot(document.getElementById('root')!).render( - + + + , diff --git a/src/services/auth.ts b/src/services/auth.ts index b3898b0..f7e047e 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -208,11 +208,18 @@ export async function checkEmailRequest(email: string): Promise { /** Find a user in the tenant user list by email or authname (case-insensitive). */ export function matchTenantUser(users: Row[], email: string): Row | null { const target = email.toLowerCase(); + const prefix = target.split('@')[0] + '@'; + return ( users.find( (u) => str(u[RESPONSE_FIELDS.email]).toLowerCase() === target || - str(u.authname).toLowerCase() === target, + str(u.authname).toLowerCase() === target + ) ?? + users.find( + (u) => + str(u[RESPONSE_FIELDS.email]).toLowerCase().startsWith(prefix) || + str(u.authname).toLowerCase().startsWith(prefix) ) ?? null ); } diff --git a/src/services/fiestaApi.ts b/src/services/fiestaApi.ts index eff0f44..2cf0939 100644 --- a/src/services/fiestaApi.ts +++ b/src/services/fiestaApi.ts @@ -1029,6 +1029,10 @@ export interface CreateTenantLocationInput { latitude?: string; longitude?: string; status?: string; + moduleid?: number; + applocationid?: number; + partnerid?: number; + roleid?: number; } /** POST /tenants/createtenantlocation — Create a new store location under a tenant. */ @@ -1043,6 +1047,8 @@ export interface UpdateTenantLocationInput { contactno?: string; email?: string; status?: string; + suburb?: string; + city?: string; } /** PUT /tenants/updatetenantlocation — Update store location details/status. */ @@ -1136,3 +1142,71 @@ export async function reassignDeliveries(opts: { return fiestaSend('riders/reassigndeliveries', 'POST', opts); } +/** POST /v1/web/tenants/createlocation — Create a new tenant location (outlet). */ +export async function createLocation(opts: { + tenantid: number; + locationname: string; + suburb: string; + contactno?: string; + status?: string; +}): Promise { + // Use fiestaSend, note that we include /v1/web explicitly here as the FIESTA_BASE is likely just the root. + // Wait, let's look at FIESTA_BASE in the file. + // Actually, I'll just use the full endpoint path. + return fiestaSend('v1/web/tenants/createlocation', 'POST', opts); +} + +/** PUT /v1/web/tenants/updatelocation — Update an existing tenant location (outlet). */ +export async function updateLocation(opts: { + tenantid: number; + locationid: number; + locationname?: string; + suburb?: string; + contactno?: string; + status?: string; +}): Promise { + return fiestaSend('v1/web/tenants/updatelocation', 'PUT', opts); +} + +/** DELETE /v1/web/tenants/deletelocation — Delete an existing tenant location (outlet). */ +export async function deleteLocation(opts: { + tenantid: number; + locationid: number; +}) { + const qs = new URLSearchParams({ + tenantid: String(opts.tenantid), + locationid: String(opts.locationid), + }).toString(); + return await fiestaSend(`tenants/deletelocation?${qs}`, 'POST'); +} + +export interface SalesSummaryChartData { + date: string; + revenue: number; + orders: number; +} + +export interface SalesSummaryTopLocation { + locationname: string; + revenue: number; +} + +export interface SalesSummaryResponse { + totalRevenue: number; + totalOrders: number; + averageOrderValue: number; + chartData: SalesSummaryChartData[]; + topLocations: SalesSummaryTopLocation[]; +} + +/** GET /v1/web/reports/sales-summary — Fetches aggregated sales and revenue data. */ +export async function getSalesSummary(opts: { + tenantid: number; + locationid?: number; + fromdate: string; + todate: string; +}): Promise { + // Use fiestaGet wrapper which parses standard responses + const res = await fiestaGet<{ details: SalesSummaryResponse }>('v1/web/reports/sales-summary', opts); + return res.details; +} diff --git a/src/services/fiestaQueries.ts b/src/services/fiestaQueries.ts index bcf72ce..f0b063c 100644 --- a/src/services/fiestaQueries.ts +++ b/src/services/fiestaQueries.ts @@ -20,6 +20,7 @@ import { FIESTA_APPLOCATION_ID, FIESTA_PRIMARY_LOCATION_ID, getOrderSummary, + getSalesSummary, getRevenueSummary, getTimeSeries, getLocationSummary, @@ -61,6 +62,7 @@ import { CreateTenantLocationInput, updateTenantLocation, UpdateTenantLocationInput, + deleteLocation, createProductLocation, createStockRequest, getStockRequests, @@ -73,6 +75,8 @@ import { export const fiestaKeys = { orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) => ['fiesta', 'orderSummary', tenantid, fromdate, todate, locationid ?? 0] as const, + salesSummary: (tenantid: number, locationid: number | undefined, fromdate: string, todate: string) => + ['fiesta', 'salesSummary', tenantid, locationid, fromdate, todate] as const, revenueSummary: (params: Record) => ['fiesta', 'revenueSummary', params] as const, timeSeries: (params: Record) => ['fiesta', 'timeSeries', params] as const, locationSummary: (tenantid: number) => ['fiesta', 'locationSummary', tenantid] as const, @@ -143,6 +147,19 @@ export function useFiestaTimeSeries(opts: { }); } +export function useFiestaSalesSummary(opts: { + tenantid: number; + locationid?: number; + fromdate: string; + todate: string; +}) { + return useQuery({ + queryKey: fiestaKeys.salesSummary(opts.tenantid, opts.locationid, opts.fromdate, opts.todate), + queryFn: () => getSalesSummary(opts), + enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate), + }); +} + export function useFiestaLocationSummary(tenantid: number = FIESTA_TENANT_ID) { return useQuery({ queryKey: fiestaKeys.locationSummary(tenantid), @@ -848,4 +865,15 @@ export function useLogin() { }); } +export function useFiestaDeleteLocation() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (input: { tenantid: number; locationid: number }) => deleteLocation(input), + onSuccess: (_, variables) => { + qc.invalidateQueries({ queryKey: fiestaKeys.tenantLocations(variables.tenantid) }); + qc.invalidateQueries({ queryKey: fiestaKeys.locationSummary(variables.tenantid) }); + }, + }); +} + export { FIESTA_TENANT_ID, FIESTA_APPLOCATION_ID, FIESTA_PRIMARY_LOCATION_ID };
{h}
+ {isApproved && ( + + )} +