stock request

This commit is contained in:
2026-07-04 17:46:30 +05:30
parent c6a3ea51da
commit 7cd0ae85ef
23 changed files with 1293 additions and 570 deletions

3
go.work Normal file
View File

@@ -0,0 +1,3 @@
go 1.21
use ..\fiesta-backend\backend_fiesta

156
go.work.sum Normal file
View File

@@ -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=

58
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"react": "^19.0.1", "react": "^19.0.1",
"react-dom": "^19.0.1", "react-dom": "^19.0.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-router-dom": "^7.2.0",
"recharts": "^3.8.1", "recharts": "^3.8.1",
"vite": "^6.2.3" "vite": "^6.2.3"
}, },
@@ -3722,6 +3723,57 @@
"node": ">=0.10.0" "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": { "node_modules/recharts": {
"version": "3.8.1", "version": "3.8.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
@@ -3922,6 +3974,12 @@
"node": ">= 0.8.0" "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": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",

View File

@@ -25,6 +25,7 @@
"react": "^19.0.1", "react": "^19.0.1",
"react-dom": "^19.0.1", "react-dom": "^19.0.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-router-dom": "^7.2.0",
"recharts": "^3.8.1", "recharts": "^3.8.1",
"vite": "^6.2.3" "vite": "^6.2.3"
}, },

12
scratch/test.js Normal file
View File

@@ -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();

View File

@@ -4,6 +4,7 @@
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { import {
Network, Network,
Truck, Truck,
@@ -29,7 +30,8 @@ import {
Layers, Layers,
Store, Store,
Settings, Settings,
LayoutDashboard LayoutDashboard,
Users
} from 'lucide-react'; } from 'lucide-react';
import { MainSection } from './types'; import { MainSection } from './types';
@@ -37,9 +39,10 @@ import {
useFiestaTenantLocations, useFiestaTenantLocations,
useFiestaLocationSummary, useFiestaLocationSummary,
useFiestaUpdateLocation, useFiestaUpdateLocation,
useFiestaCreateLocation,
useFiestaOrderSummary, useFiestaOrderSummary,
} from './services/fiestaQueries'; } 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 Sidebar from './components/Sidebar';
import Header from './components/Header'; import Header from './components/Header';
import DashboardView from './components/DashboardView'; import DashboardView from './components/DashboardView';
@@ -97,13 +100,14 @@ export default function App() {
}; };
// Navigation indicators states // Navigation indicators states
const [currentSection, setCurrentSection] = useState<MainSection>('dashboard'); const location = useLocation();
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 navigate = useNavigate();
const handleSetSection = (sec: MainSection) => { // Derive current section from URL path, default to 'dashboard'
setCurrentSection(sec); const pathParts = location.pathname.split('/');
setSelectedStore(null); 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 [isCoimbatoreView, setIsCoimbatoreView] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -474,31 +478,7 @@ export default function App() {
</div> </div>
</div> </div>
{/* Live Sparkline Trend Histogram */}
<div className="mt-4 pt-3.5 border-t border-zinc-100">
<div className="flex justify-between items-center text-[8px] text-zinc-400 font-bold uppercase tracking-widest mb-1.5">
<span>Speed Index (Live Feed)</span>
<span className="text-[#662582] flex items-center gap-0.5 text-[8px] font-bold">
<Activity className="w-2.5 h-2.5 animate-pulse" /> Live
</span>
</div>
<div className="h-6 flex items-end gap-1">
{[30, 48, 25, 62, 54, 75, 42, 80, Math.min(95, Math.max(15, st.deliveries * 1.8))].map((val, idx) => (
<div key={idx} className="flex-1 bg-zinc-100 rounded-t-sm h-full relative overflow-hidden group/bar">
<div
style={{ height: `${val}%` }}
className={`absolute bottom-0 left-0 right-0 rounded-t-sm transition-all duration-500 ${
st.status.toLowerCase() === 'active'
? st.deliveries > 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'
}`}
/>
</div>
))}
</div>
</div>
{/* Lead Manager Profile block */} {/* Lead Manager Profile block */}
<div className="flex justify-between items-center text-xs text-zinc-650 bg-zinc-50/80 rounded-xl p-2.5 border border-zinc-100/80 mt-4"> <div className="flex justify-between items-center text-xs text-zinc-650 bg-zinc-50/80 rounded-xl p-2.5 border border-zinc-100/80 mt-4">
@@ -567,22 +547,28 @@ export default function App() {
}; };
// ── Auth gate ────────────────────────────────────────────────────────────── // ── 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) { if (authRole === null) {
return <LoginView onLogin={handleLogin} />; return (
} <Routes>
if (authRole === 'user' && authUser) { <Route path="/login" element={<LoginView onLogin={handleLogin} />} />
return <UserStorePage onLogout={handleLogout} user={authUser} />; <Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
);
} }
if (authRole === 'user' && authUser) {
return ( return (
<div className="min-h-screen bg-[#f8fafc] text-[#0f172a] font-sans antialiased"> <Routes>
<Route path="/store/*" element={<UserStorePage onLogout={handleLogout} user={authUser} />} />
<Route path="*" element={<Navigate to="/store/console" replace />} />
</Routes>
);
}
const AdminConsole = (
<div className="min-h-screen bg-[#f8fafc] text-[#0f172a] font-sans antialiased overflow-x-hidden">
{/* Navbar segment */} {/* Navbar segment */}
<Header <Header
currentSection={currentSection}
setCurrentSection={handleSetSection}
isCoimbatoreView={isCoimbatoreView} isCoimbatoreView={isCoimbatoreView}
onToggleSidebar={() => setSidebarOpen((prev) => !prev)} onToggleSidebar={() => setSidebarOpen((prev) => !prev)}
isSidebarOpen={sidebarOpen} isSidebarOpen={sidebarOpen}
@@ -610,23 +596,23 @@ export default function App() {
/> />
{/* Main Container workspace layout splits */} {/* Main Container workspace layout splits */}
<div className="flex pt-16"> <div className="flex pt-16 w-full max-w-[100vw]">
{/* Interactive Left Rail */} {/* Interactive Left Rail */}
<Sidebar <Sidebar
currentSection={currentSection}
setCurrentSection={handleSetSection}
isCoimbatoreView={isCoimbatoreView} isCoimbatoreView={isCoimbatoreView}
setIsCoimbatoreView={setIsCoimbatoreView} setIsCoimbatoreView={setIsCoimbatoreView}
isOpen={sidebarOpen} isOpen={sidebarOpen}
isAdmin={authRole === 'admin'} isAdmin={authRole === 'admin'}
onClose={() => setSidebarOpen(false)}
/> />
{/* Main core pages payload area */} {/* Main core pages payload area */}
<main className={`flex-1 min-w-0 transition-all duration-300 ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'} ${currentSection === 'inventory' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]'}`}> <main className={`flex-1 min-w-0 transition-all duration-300 ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'} ${currentSection === 'inventory' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]'}`}>
<div className={`w-full transition-all duration-300 ${currentSection === 'inventory' ? 'h-full p-4 md:p-6 overflow-hidden' : 'p-container-margin md:p-xl space-y-lg'}`}> <div className={`w-full transition-all duration-300 ${currentSection === 'inventory' ? 'h-full p-4 md:p-6 overflow-hidden' : 'p-container-margin md:p-xl space-y-lg'}`}>
{/* Nav content routing */} <Routes>
{currentSection === 'dashboard' && ( <Route index element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={
selectedStore ? ( selectedStore ? (
renderStoresSection() renderStoresSection()
) : ( ) : (
@@ -637,33 +623,42 @@ export default function App() {
</div> </div>
</div> </div>
) )
)} } />
{currentSection === 'inventory' && ( <Route path="inventory" element={
<InventoryView <InventoryView
searchQuery={searchQuery} searchQuery={searchQuery}
isCoimbatoreView={isCoimbatoreView} isCoimbatoreView={isCoimbatoreView}
tenantId={tenantId} tenantId={tenantId}
/> />
)} } />
{currentSection === 'reports' && ( <Route path="reports" element={
<ReportsView <ReportsView
searchQuery={searchQuery} searchQuery={searchQuery}
isCoimbatoreView={isCoimbatoreView} isCoimbatoreView={isCoimbatoreView}
setIsCoimbatoreView={setIsCoimbatoreView} setIsCoimbatoreView={setIsCoimbatoreView}
tenantId={tenantId} tenantId={tenantId}
/> />
)} } />
<Route path="settings" element={
<SettingsView tenantId={tenantId} user={authUser ?? undefined} />
} />
{/* Handle alternative sections: Settings */} </Routes>
{['settings'].includes(currentSection) &&
renderSecondarySection()
}
</div> </div>
</main> </main>
</div> </div>
</div>
);
return (
<>
<Routes>
<Route path="/admin/*" element={AdminConsole} />
<Route path="*" element={<Navigate to="/admin/dashboard" replace />} />
</Routes>
{/* CALENDAR SCHEDULER DIALOG MODAL */} {/* CALENDAR SCHEDULER DIALOG MODAL */}
{showCalendarModal && ( {showCalendarModal && (
@@ -798,6 +793,6 @@ export default function App() {
)} )}
<ComparisonModal /> <ComparisonModal />
</div> </>
); );
} }

View File

@@ -151,6 +151,10 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
const res = await createLocationMut.mutateAsync({ const res = await createLocationMut.mutateAsync({
...storeForm, ...storeForm,
status: 'Active', status: 'Active',
moduleid: 2,
applocationid: 1,
partnerid: 1,
roleid: 4,
}); });
setStoreSuccess(res); setStoreSuccess(res);
alert('Store Location created successfully!'); alert('Store Location created successfully!');

View File

@@ -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 (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between md:items-end gap-4">
<div>
<h2 className="text-2xl font-black text-[#0f172a] flex items-center gap-2">
<Users size={26} className="text-[#662582]" />
Customers
</h2>
</div>
<div className="relative w-full md:w-72 shrink-0">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-[#662582]" />
<input
type="text"
placeholder="Search customers, stores..."
value={customerSearch}
onChange={(e) => 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"
/>
</div>
</div>
{customersQ.isLoading ? (
<div className="h-64 flex flex-col items-center justify-center space-y-4">
<div className="relative w-10 h-10">
<span className="absolute inset-0 rounded-full border-2 border-purple-100" />
<span className="absolute inset-0 rounded-full border-2 border-t-purple-600 animate-spin" />
</div>
<p className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Loading Customers...</p>
</div>
) : customersList.length === 0 ? (
<div className="bg-white border border-[#e2e8f0] rounded-3xl py-24 flex flex-col items-center gap-4 text-center px-6 shadow-sm">
<span className="flex items-center justify-center w-16 h-16 rounded-2xl bg-purple-50 text-purple-700 shadow-sm"><Users size={28} /></span>
<p className="text-lg font-bold text-[#0f172a]">No customers found</p>
<p className="text-sm text-zinc-500 max-w-[350px] leading-relaxed">
{customerSearch ? 'Nothing matches your search criteria. Try a different term.' : 'There are no customers registered in the tenant yet.'}
</p>
{customerSearch && (
<button onClick={() => setCustomerSearch('')} className="mt-2 text-[13px] font-bold text-purple-700 bg-purple-50 hover:bg-purple-100 px-4 py-2 rounded-lg transition-colors cursor-pointer">
Clear Search
</button>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{customersList.map((c: any, idx: number) => {
const tone = toneFor(c.name || `c${idx}`);
const locality = localityOf(c.address);
return (
<div key={c.id ?? idx} className="group bg-white border border-[#e2e8f0] rounded-2xl p-5 hover:shadow-lg hover:border-purple-300 transition-all duration-300 flex flex-col">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3 min-w-0">
<span
className="w-12 h-12 rounded-2xl flex items-center justify-center font-bold text-sm shrink-0 border border-white"
style={{ background: tone.soft, color: tone.fg }}
>
{initialsOf(c.name)}
</span>
<div className="min-w-0">
<h3 className="font-extrabold text-[#0f172a] text-[15px] truncate" title={c.name}>{c.name}</h3>
{locality && (
<p className="text-[11px] text-zinc-500 mt-0.5 flex items-center gap-1 font-medium truncate" title={locality}>
<MapPin size={10} className="shrink-0 text-zinc-400" /> {locality}
</p>
)}
</div>
</div>
{c.ordersCount > 5 && (
<span className="shrink-0 ml-2 px-2 py-1 bg-amber-50 text-amber-700 text-[9px] font-black uppercase tracking-widest rounded-lg border border-amber-200 flex items-center gap-1">
<Award size={10} /> Loyal
</span>
)}
</div>
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-slate-50 rounded-xl p-3 border border-slate-100">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest block">Orders</span>
<span className="font-black text-[16px] text-[#0f172a] mt-0.5 block">{c.ordersCount}</span>
</div>
<div className="bg-emerald-50/50 rounded-xl p-3 border border-emerald-100/50">
<span className="text-[9px] font-bold text-emerald-600/70 uppercase tracking-widest block">Gross Spend</span>
<span className="font-black text-[16px] text-emerald-700 mt-0.5 block">{c.totalSpent}</span>
</div>
</div>
<div className="space-y-2 mb-5 flex-1">
<div className="flex items-center gap-2 text-xs text-slate-600 font-medium">
<div className="w-6 h-6 rounded-lg bg-slate-100 flex items-center justify-center shrink-0"><Phone size={12} className="text-slate-400" /></div>
<span className="tabular-nums">{c.phone}</span>
</div>
{c.email && c.email !== '—' && (
<div className="flex items-center gap-2 text-xs text-slate-600 font-medium">
<div className="w-6 h-6 rounded-lg bg-slate-100 flex items-center justify-center shrink-0"><Mail size={12} className="text-slate-400" /></div>
<span className="truncate" title={c.email}>{c.email}</span>
</div>
)}
</div>
<div className="pt-4 border-t border-slate-100 flex items-center justify-between gap-3 mt-auto">
<span className="text-xs font-bold text-purple-700 bg-purple-50 px-3 py-1.5 rounded-lg truncate w-full text-center">
{c.storeName}
</span>
</div>
</div>
);
})}
</div>
)}
</div>
);
}

View File

@@ -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 (
<div
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-4 animate-in fade-in duration-200"
onClick={(e) => { if (e.target === e.currentTarget && !isLoading) onCancel(); }}
>
<div
className="bg-white border border-[#e2e8f0] rounded-xl w-[90vw] sm:w-[400px] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 sm:p-5 border-b border-[#e2e8f0] bg-slate-50/50 flex justify-between items-center shrink-0">
<div className="flex items-center gap-2">
{isDestructive && <AlertTriangle size={18} className="text-red-500" />}
<h2 className="text-base font-bold text-slate-800 uppercase tracking-wide">{title}</h2>
</div>
<button
onClick={onCancel}
disabled={isLoading}
className="text-slate-400 hover:text-slate-600 hover:bg-slate-100 p-1.5 rounded-lg transition-colors disabled:opacity-50"
>
<X size={18} />
</button>
</div>
<div className="p-5 overflow-y-auto">
<div className="text-sm text-slate-600 leading-relaxed">
{message}
</div>
</div>
<div className="p-4 bg-slate-50/80 border-t border-slate-100 flex justify-end gap-3 shrink-0">
<button
type="button"
onClick={onCancel}
disabled={isLoading}
className="px-4 py-2 text-sm font-semibold text-slate-600 hover:text-slate-800 hover:bg-slate-200 bg-slate-100 rounded-lg transition-colors disabled:opacity-50"
>
{cancelText}
</button>
<button
type="button"
onClick={onConfirm}
disabled={isLoading}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition-colors flex items-center justify-center min-w-[90px] disabled:opacity-50 ${
isDestructive
? 'bg-red-600 hover:bg-red-700 text-white shadow-sm shadow-red-200'
: 'bg-purple-600 hover:bg-purple-700 text-white shadow-sm shadow-purple-200'
}`}
>
{isLoading ? (
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
confirmText
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -490,7 +490,7 @@ function OrdersSummaryReport({ tenantId, locationid, fromdate, todate }: { tenan
// ── Riders Summary (per rider) ─────────────────────────────────────────────────── // ── Riders Summary (per rider) ───────────────────────────────────────────────────
function RidersSummaryReport({ fromdate, todate, tenantId, locationid }: { fromdate: string; todate: string; tenantId: number; locationid?: number }) { 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 rows = q.data ?? [];
const mapped = rows.map((r) => ({ const mapped = rows.map((r) => ({
name: fstr(r.fullname) || `${fstr(r.firstname)} ${fstr(r.lastname)}`.trim() || fstr(r.username) || `Rider ${fstr(r.userid)}`, name: fstr(r.fullname) || `${fstr(r.firstname)} ${fstr(r.lastname)}`.trim() || fstr(r.username) || `Rider ${fstr(r.userid)}`,

View File

@@ -42,6 +42,7 @@ import {
useFiestaProductCategories, useFiestaProductCategories,
useFiestaMasterCatalog, useFiestaMasterCatalog,
useFiestaUpdateStockRequest, useFiestaUpdateStockRequest,
useFiestaGetStockRequests,
} from '../services/fiestaQueries'; } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi'; import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi';
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers'; 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 { SlideDrawer, Skeleton, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND, tint, edge, StatusChip } from './consoleUi';
import FMCGHoverOverlay from './FMCGHoverOverlay'; import FMCGHoverOverlay from './FMCGHoverOverlay';
import { useCompare } from '../contexts/CompareContext'; import { useCompare } from '../contexts/CompareContext';
import TrialBatchDrawer, { TrialProduct } from './TrialBatchDrawer';
const MOCK_GLOBAL_CATALOG: ProductMatrixItem[] = [ const MOCK_GLOBAL_CATALOG: ProductMatrixItem[] = [
{ {
@@ -122,7 +122,7 @@ export default function InventoryView({
tenantId = FIESTA_TENANT_ID tenantId = FIESTA_TENANT_ID
}: InventoryViewProps) { }: InventoryViewProps) {
const { selectedProducts, toggleProduct, setIsComparing, clearSelection, setHideCompareBar } = useCompare(); const { selectedProducts, toggleProduct, setIsComparing, clearSelection, setHideCompareBar } = useCompare();
const [trialProducts, setTrialProducts] = useState<TrialProduct[]>([]); const [searchTerm, setSearchTerm] = useState('');
// ── Live stock across every outlet (Fiesta) ─────────────────────────────── // ── Live stock across every outlet (Fiesta) ───────────────────────────────
// This page is the admin's command surface. The GLOBAL CATALOG is the deduped // 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 [requestStoreFilter, setRequestStoreFilter] = useState('All Stores');
const [storeRequests, setStoreRequests] = useState<{ locationid: number, locationname: string, picks: Record<string, { qty: number; status: 'Pending' | 'Approved' | 'Rejected' | 'Cancelled'; requestedAt: string; resolvedAt?: string }> }[]>([]); const [storeRequests, setStoreRequests] = useState<{ locationid: number, locationname: string, picks: Record<string, { qty: number; status: 'Pending' | 'Approved' | 'Rejected' | 'Cancelled'; requestedAt: string; resolvedAt?: string }> }[]>([]);
const stockRequestsQ = useFiestaGetStockRequests({ tenantid: tenantId, pagesize: 1000 });
useEffect(() => { useEffect(() => {
if (activeTab === 'requests') { if (activeTab === 'requests' && stockRequestsQ.data) {
const reqs: any[] = []; const reqsMap: Record<number, any> = {};
for (let i = 0; i < localStorage.length; i++) { stockRequestsQ.data.forEach((req: any) => {
const key = localStorage.key(i); if (!reqsMap[req.locationid]) {
if (key && key.startsWith('nearledaily.catalogue.request.')) { const loc = locations.find(l => l.locationid === req.locationid);
const locIdStr = key.split('.').pop(); reqsMap[req.locationid] = {
if (locIdStr && locIdStr !== 'na') { locationid: req.locationid,
const locId = parseInt(locIdStr); locationname: loc ? loc.locationname : `Outlet #${req.locationid}`,
const raw = localStorage.getItem(key); picks: {}
if (raw) { };
try {
const parsed = JSON.parse(raw);
const picks: Record<string, any> = {};
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 (!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
};
} }
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
}); });
setStoreRequests(Object.values(reqsMap).filter(r => Object.keys(r.picks).length > 0));
} }
} catch (e) {} }, [activeTab, locations, stockRequestsQ.data]);
}
}
}
}
setStoreRequests(reqs);
}
}, [activeTab, locations]);
const updateStockRequestMutation = useFiestaUpdateStockRequest(); const updateStockRequestMutation = useFiestaUpdateStockRequest();
const updateProductRequestStatus = (locationid: number, productid: string, status: 'Approved' | 'Rejected' | 'Pending') => { 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({ updateStockRequestMutation.mutate({
tenantid: tenantId, tenantid: tenantId,
locationid, locationid,
productid: Number(productid), productid: Number(productid),
requestid,
status status
}); });
const key = `nearledaily.catalogue.request.${locationid}`; // Optimistic UI update
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 => { setStoreRequests(prev => prev.map(r => {
if (r.locationid === locationid) { if (r.locationid === locationid && r.picks[productid]) {
return { ...r, picks: { ...r.picks, [productid]: picks[productid] } }; return {
...r,
picks: {
...r.picks,
[productid]: { ...r.picks[productid], status, resolvedAt: new Date().toISOString() }
}
};
} }
return r; return r;
})); }));
}
} catch (e) {}
}
}; };
// Hide compare bar when not in Global Catalogue // Hide compare bar when not in Global Catalogue
@@ -1020,22 +991,6 @@ export default function InventoryView({
<Plus size={14} /> Import to Catalogue <Plus size={14} /> Import to Catalogue
</button> </button>
)} )}
<button
onClick={(e) => {
e.stopPropagation();
setTrialProducts([{
id: prod.id,
name: prod.name,
sku: prod.sku,
category: prod.category.split(' / ')[0],
price: prod.revenue / Math.max(1, prod.unitsSold),
image: prod.image
}]);
}}
className="w-full flex items-center justify-center gap-1.5 py-2 rounded-xl text-[11px] font-bold transition-all bg-indigo-50 text-indigo-700 hover:bg-indigo-600 hover:text-white cursor-pointer border border-indigo-100 hover:border-indigo-600"
>
<Package size={14} /> Request Sample
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1185,22 +1140,6 @@ export default function InventoryView({
<Plus size={14} /> Import to Catalogue <Plus size={14} /> Import to Catalogue
</button> </button>
)} )}
<button
onClick={(e) => {
e.stopPropagation();
setTrialProducts([{
id: prod.id,
name: prod.name,
sku: prod.sku,
category: prod.category.split(' / ')[0],
price: prod.revenue / Math.max(1, prod.unitsSold),
image: prod.image
}]);
}}
className="w-full flex items-center justify-center gap-1.5 py-2 rounded-xl text-[11px] font-bold transition-all bg-indigo-50 text-indigo-700 hover:bg-indigo-600 hover:text-white cursor-pointer border border-indigo-100 hover:border-indigo-600"
>
<Package size={14} /> Request Sample
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1263,7 +1202,7 @@ export default function InventoryView({
<Inbox size={32} className="text-slate-400" /> <Inbox size={32} className="text-slate-400" />
</div> </div>
<h3 className="text-xl font-bold text-slate-700 tracking-tight">No Pending Requests</h3> <h3 className="text-xl font-bold text-slate-700 tracking-tight">No Pending Requests</h3>
<p className="text-sm font-medium text-slate-400 mt-2 text-center max-w-sm leading-relaxed"> <p className="text-sm font-medium text-slate-400 mt-2 text-center w-full max-w-[400px] leading-relaxed">
You're all caught up! All store stock requests have been processed and there is currently no pending action required. You're all caught up! All store stock requests have been processed and there is currently no pending action required.
</p> </p>
</div> </div>
@@ -1505,22 +1444,6 @@ export default function InventoryView({
> >
<Plus size={18} strokeWidth={2} /> Import Local <Plus size={18} strokeWidth={2} /> Import Local
</button> </button>
<button
onClick={() => {
setTrialProducts([{
id: selectedAdminProduct.id,
name: selectedAdminProduct.name,
sku: selectedAdminProduct.sku,
category: String(selectedAdminProduct.category || '').split(' / ')[0],
price: selectedAdminProduct.revenue / Math.max(1, selectedAdminProduct.unitsSold),
image: selectedAdminProduct.image
}]);
}}
className="w-auto px-6 flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-colors bg-white text-slate-700 border border-slate-200 hover:bg-slate-50"
title="Request Sample"
>
<Package size={18} strokeWidth={2} /> Sample
</button>
</div> </div>
)} )}
@@ -1661,23 +1584,6 @@ export default function InventoryView({
})()} })()}
</SlideDrawer> </SlideDrawer>
<TrialBatchDrawer
isOpen={trialProducts.length > 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 ── */} {/* ── Floating Import Button ── */}
{activeTab === 'import_branding' && selectedProducts.length > 0 && ( {activeTab === 'import_branding' && selectedProducts.length > 0 && (
<button <button

View File

@@ -54,6 +54,7 @@ const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'S
export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimbatoreView, tenantId = FIESTA_TENANT_ID }: ReportsViewProps) { export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimbatoreView, tenantId = FIESTA_TENANT_ID }: ReportsViewProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState('This Year (YTD)'); const [selectedTimeframe, setSelectedTimeframe] = useState('This Year (YTD)');
const [selectedRegion, setSelectedRegion] = useState<'all' | 'coimbatore' | 'chennai' | 'bangalore'>('all'); const [selectedRegion, setSelectedRegion] = useState<'all' | 'coimbatore' | 'chennai' | 'bangalore'>('all');
const [activeStoreId, setActiveStoreId] = useState<number | 'all'>('all');
const [stockFilter, setStockFilter] = useState<'All' | 'Healthy' | 'Low Stock' | 'Critical'>('All'); const [stockFilter, setStockFilter] = useState<'All' | 'Healthy' | 'Low Stock' | 'Critical'>('All');
const [selectedCell, setSelectedCell] = useState<{ day: string; hour: string; val: number } | null>(null); const [selectedCell, setSelectedCell] = useState<{ day: string; hour: string; val: number } | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@@ -105,7 +106,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
const locSummaryQ = useFiestaLocationSummary(tenantId); const locSummaryQ = useFiestaLocationSummary(tenantId);
const filteredLocations = useMemo(() => { const regionLocations = useMemo(() => {
const rawLocations = [...(locSummaryQ.data ?? [])]; const rawLocations = [...(locSummaryQ.data ?? [])];
if (selectedRegion === 'coimbatore') { if (selectedRegion === 'coimbatore') {
return rawLocations.filter(r => isCoimbatoreNode(r.locationname || '')); return rawLocations.filter(r => isCoimbatoreNode(r.locationname || ''));
@@ -116,6 +117,15 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
return rawLocations; return rawLocations;
}, [locSummaryQ.data, selectedRegion]); }, [locSummaryQ.data, selectedRegion]);
const effectiveStoreId = activeTab === 'sales_revenue' ? activeStoreId : 'all';
const filteredLocations = useMemo(() => {
if (effectiveStoreId !== 'all') {
return regionLocations.filter(r => Number(r.locationid) === effectiveStoreId);
}
return regionLocations;
}, [regionLocations, effectiveStoreId]);
const mappedLocations = useMemo(() => { const mappedLocations = useMemo(() => {
return filteredLocations.map(l => ({ locationid: Number(l.locationid), locationname: l.locationname || '' })); return filteredLocations.map(l => ({ locationid: Number(l.locationid), locationname: l.locationname || '' }));
}, [filteredLocations]); }, [filteredLocations]);
@@ -139,10 +149,10 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
return ((current - previous) / previous) * 100; return ((current - previous) / previous) * 100;
}; };
const isAllRegions = selectedRegion === 'all'; const isGlobalView = selectedRegion === 'all' && effectiveStoreId === 'all';
const ordersDelta = (s && prevS && isAllRegions) ? pctChange(s.total, prevS.total) : null; const ordersDelta = (s && prevS && isGlobalView) ? pctChange(s.total, prevS.total) : null;
const cancelledDelta = (s && prevS && isAllRegions) ? pctChange(s.cancelled, prevS.cancelled) : null; const cancelledDelta = (s && prevS && isGlobalView) ? pctChange(s.cancelled, prevS.cancelled) : null;
const revenueDelta = (revS && prevRevS && isAllRegions) ? pctChange(revS.grossrevenue, prevRevS.grossrevenue) : null; const revenueDelta = (revS && prevRevS && isGlobalView) ? pctChange(revS.grossrevenue, prevRevS.grossrevenue) : null;
const fmtDelta = (d: number) => `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`; const fmtDelta = (d: number) => `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`;
@@ -204,7 +214,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
let avgOrderVal = 0; let avgOrderVal = 0;
let activeSkusVal = 0; let activeSkusVal = 0;
if (isAllRegions) { if (isGlobalView) {
totalOrdersVal = s?.total ?? 0; totalOrdersVal = s?.total ?? 0;
deliveredVal = s?.delivered ?? 0; deliveredVal = s?.delivered ?? 0;
cancelledVal = s?.cancelled ?? 0; cancelledVal = s?.cancelled ?? 0;
@@ -257,7 +267,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
isPositive: revenueDelta === null ? true : revenueDelta >= 0, isPositive: revenueDelta === null ? true : revenueDelta >= 0,
spark: [20, 30, 25, 45, 40, 55, 50, 68], spark: [20, 30, 25, 45, 40, 55, 50, 68],
color: 'emerald', color: 'emerald',
awaiting: !isAllRegions, awaiting: !isGlobalView,
}, },
{ {
id: 'cancelled' as const, id: 'cancelled' as const,
@@ -456,7 +466,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
); );
const chartData = useMemo(() => { const chartData = useMemo(() => {
const data = tsQ_all.data || []; const data = isGlobalView ? (tsQ_all.data || []) : (tsQ_stores.data || []);
if (data.length === 0) return [{ label: 'No Data', value: 0 }]; if (data.length === 0) return [{ label: 'No Data', value: 0 }];
return data.map((d: any) => { return data.map((d: any) => {
@@ -483,27 +493,48 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
value: val value: val
}; };
}); });
}, [tsQ_all.data, tsQ_stores.data, isAllRegions, chartMetric, tsGranularity]); }, [tsQ_all.data, tsQ_stores.data, isGlobalView, chartMetric, tsGranularity]);
const maxChartVal = useMemo(() => Math.max(...chartData.map(d => d.value), 1), [chartData]); const maxChartVal = useMemo(() => Math.max(...chartData.map(d => d.value), 1), [chartData]);
return ( return (
<div className="space-y-lg animate-in fade-in duration-500 font-sans"> <div className="space-y-lg animate-in fade-in duration-500 font-sans">
<div className="flex items-center gap-4 border-b border-slate-200/70 pb-2 -mt-2 relative z-20"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 relative z-20 -mt-2 pb-2 border-b border-slate-200/70">
<div className="flex items-center gap-4">
<button <button
onClick={() => setActiveTab('overview')} onClick={() => setActiveTab('overview')}
className={`pb-2 text-sm font-bold border-b-2 transition-colors ${activeTab === 'overview' ? 'border-[#662582] text-[#662582]' : 'border-transparent text-slate-500 hover:text-slate-700'}`} className={`pb-1 text-sm font-bold border-b-2 transition-colors ${activeTab === 'overview' ? 'border-[#662582] text-[#662582]' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
> >
Overview Overview
</button> </button>
<button <button
onClick={() => setActiveTab('sales_revenue')} onClick={() => setActiveTab('sales_revenue')}
className={`pb-2 text-sm font-bold border-b-2 transition-colors ${activeTab === 'sales_revenue' ? 'border-[#662582] text-[#662582]' : 'border-transparent text-slate-500 hover:text-slate-700'}`} className={`pb-1 text-sm font-bold border-b-2 transition-colors ${activeTab === 'sales_revenue' ? 'border-[#662582] text-[#662582]' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
> >
Sales & Revenue Sales & Revenue
</button> </button>
</div> </div>
{/* Global Store Filter */}
{activeTab === 'sales_revenue' && (
<div className="flex items-center gap-2">
<Store size={16} className="text-slate-500" />
<select
value={activeStoreId}
onChange={(e) => setActiveStoreId(e.target.value === 'all' ? 'all' : Number(e.target.value))}
className="text-sm font-bold outline-none rounded-lg px-3 py-1 border border-slate-200 bg-white text-slate-700 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all cursor-pointer"
>
<option value="all">All Stores</option>
{regionLocations.map(loc => (
<option key={loc.locationid} value={Number(loc.locationid)}>
{loc.locationname || `Store ${loc.locationid}`}
</option>
))}
</select>
</div>
)}
</div>
{activeTab === 'overview' ? ( {activeTab === 'overview' ? (
<> <>
{/* Primary KPI Row - 4 Key Tab buttons with Sparklines */} {/* Primary KPI Row - 4 Key Tab buttons with Sparklines */}
@@ -1096,7 +1127,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
)} )}
</> </>
) : ( ) : (
<SalesRevenueReport tenantId={tenantId} /> <SalesRevenueReport tenantId={tenantId} locationid={activeStoreId === 'all' ? undefined : Number(activeStoreId)} />
)} )}
</div> </div>
); );

View File

@@ -17,55 +17,11 @@ import {
TrendingDown TrendingDown
} from 'lucide-react'; } from 'lucide-react';
import { import {
useFiestaAllOrders useFiestaAllOrders,
useFiestaSalesSummary
} from '../services/fiestaQueries'; } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi'; 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 ... // ... inside the component ...
import { shortTime } from '../services/fiestaMappers'; 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'; const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom';
// ── Queries ────────────────────────────────────────────────────────────────── // ── Queries ──────────────────────────────────────────────────────────────────
const salesSummaryQ = useFiestaSalesSummary({ tenantid: tenantId, fromdate, todate, locationid });
const allOrdersQ = useFiestaAllOrders({ 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 || [];
const allRows = allOrdersQ.data && allOrdersQ.data.length > 0 ? allOrdersQ.data : mockData;
// ── Calculations ───────────────────────────────────────────────────────────── // ── 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(() => { const completedOrders = useMemo(() => {
return allRows.filter((r) => { return allRows.filter((r) => {
const s = fstr(r.orderstatus).toLowerCase(); const s = fstr(r.orderstatus).toLowerCase();
@@ -121,52 +84,6 @@ export default function SalesRevenueReport({
}); });
}, [allRows]); }, [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<string, { date: string; revenue: number; orders: number }>();
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 formatLabel = (key: string) => {
const d = new Date(key); const d = new Date(key);
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
@@ -234,19 +151,9 @@ export default function SalesRevenueReport({
}, [completedOrders, locationid]); }, [completedOrders, locationid]);
// 4. Top Locations Leaderboard (Admin Only) // 4. Top Locations Leaderboard (Admin Only)
const topLocations = useMemo(() => { const topLocations = backendTopLocations.length > 0
if (locationid) return []; // Only for admin ? backendTopLocations.map(l => ({ name: l.locationname, revenue: l.revenue }))
const lmap = new Map<string, number>(); : [];
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 kpis = [ const kpis = [
{ label: 'Total Sales', value: totalOrders.toLocaleString('en-IN'), color: '#6366f1', icon: <ShoppingBag size={20} />, badge: 'Completed orders' }, { label: 'Total Sales', value: totalOrders.toLocaleString('en-IN'), color: '#6366f1', icon: <ShoppingBag size={20} />, badge: 'Completed orders' },

View File

@@ -17,12 +17,13 @@ import {
Plus, Plus,
Bike Bike
} from 'lucide-react'; } from 'lucide-react';
import { useFiestaAllTenants, useFiestaTenantLocations } from '../services/fiestaQueries'; import { useFiestaAllTenants, useFiestaTenantLocations, useFiestaUpdateLocation, useFiestaDeleteLocation } from '../services/fiestaQueries';
import { useAppRoles } from '../services/queries'; import { useAppRoles } from '../services/queries';
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi'; import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
import UsersPanel from './UsersPanel'; import UsersPanel from './UsersPanel';
import AwaitingApi from './AwaitingApi'; import AwaitingApi from './AwaitingApi';
import AdminConsole from './AdminConsole'; import AdminConsole from './AdminConsole';
import { ConfirmModal } from './ConfirmModal';
import type { AuthUser } from '../services/auth'; import type { AuthUser } from '../services/auth';
type TabKey = 'profile' | 'outlets' | 'users'; 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 // (see [R6]) so they are not persisted; the operational controls that would
// need persistence show an AwaitingApi notice instead of saving silently. // need persistence show an AwaitingApi notice instead of saving silently.
const [form, setForm] = useState<MerchantSettings>({ ...DEFAULTS }); const [form, setForm] = useState<MerchantSettings>({ ...DEFAULTS });
const updateLocationMut = useFiestaUpdateLocation();
const deleteLocationMut = useFiestaDeleteLocation();
const [editingStore, setEditingStore] = useState<any>(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); const [showStoreOnboarding, setShowStoreOnboarding] = useState(false);
useEffect(() => { useEffect(() => {
@@ -210,11 +255,10 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
}, [rolesQ.data]); }, [rolesQ.data]);
return ( return (
<div className="space-y-lg animate-in fade-in duration-300 relative font-sans text-slate-700"> <div className="space-y-3 font-sans text-slate-700">
{/* Header */} {/* Header */}
<div className="pb-4 border-b border-slate-100"> <div>
<div>
<div className="mt-3">
{tenantsQ.isLoading ? ( {tenantsQ.isLoading ? (
<span className="inline-flex items-center gap-1.5 text-xs font-bold text-slate-400 uppercase tracking-wider"> <span className="inline-flex items-center gap-1.5 text-xs font-bold text-slate-400 uppercase tracking-wider">
<span className="w-2 h-2 rounded-full bg-slate-350 animate-pulse" /> Loading store profile <span className="w-2 h-2 rounded-full bg-slate-350 animate-pulse" /> Loading store profile
@@ -420,7 +464,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
<div className="space-y-4"> <div className="space-y-4">
{/* Header: Outlet name & status */} {/* Header: Outlet name & status */}
<div className="flex justify-between items-start gap-3"> <div className="flex justify-between items-start gap-3">
<div className="flex gap-3"> <div className="flex gap-3 flex-1 min-w-0">
<div className="p-3 rounded-xl bg-purple-50 text-purple-650 shrink-0 self-start group-hover:bg-purple-100 transition-colors"> <div className="p-3 rounded-xl bg-purple-50 text-purple-650 shrink-0 self-start group-hover:bg-purple-100 transition-colors">
<Store size={20} /> <Store size={20} />
</div> </div>
@@ -432,6 +476,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-2 flex-wrap shrink-0">
<span className={`shrink-0 px-2.5 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${ <span className={`shrink-0 px-2.5 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${
loc.status.toLowerCase() === 'active' loc.status.toLowerCase() === 'active'
? 'text-emerald-700 bg-emerald-50 border border-emerald-100/50' ? 'text-emerald-700 bg-emerald-50 border border-emerald-100/50'
@@ -439,23 +484,35 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
}`}> }`}>
{loc.status || '—'} {loc.status || '—'}
</span> </span>
<button
type="button"
onClick={() => {
setEditingStore({ ...loc });
setShowEditStoreModal(true);
}}
className="text-[10px] uppercase font-bold text-purple-600 hover:text-purple-800 hover:bg-purple-50 px-2 py-1 rounded border border-purple-200 transition-colors"
>
Edit
</button>
<button
type="button"
onClick={() => handleDeleteStore(loc)}
className="text-[10px] uppercase font-bold text-red-600 hover:text-red-800 hover:bg-red-50 px-2 py-1 rounded border border-red-200 transition-colors"
>
Delete
</button>
</div>
</div> </div>
{/* Outlet Details Grid */} {/* Outlet Details Grid */}
<div className="grid grid-cols-2 gap-3 bg-slate-50/50 p-3.5 rounded-xl border border-slate-100/80"> <div className="grid grid-cols-1 gap-3 bg-slate-50/50 p-3.5 rounded-xl border border-slate-100/80">
<div className="space-y-1"> <div className="space-y-1">
<span className="text-[10px] text-slate-455 uppercase font-bold block">Delivery Range</span> <span className="text-[10px] text-slate-455 uppercase font-bold block">Delivery Range</span>
<p className="font-bold text-slate-700 text-xs"> <p className="font-bold text-slate-700 text-xs">
{loc.deliveryradius ? `Up to ${loc.deliveryradius / 1000} km` : '—'} {loc.deliveryradius ? `Up to ${loc.deliveryradius / 1000} km` : '—'}
</p> </p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1 border-t border-slate-100 pt-2 mt-1">
<span className="text-[10px] text-slate-455 uppercase font-bold block">Delivery Speed</span>
<p className="font-bold text-slate-700 text-xs">
{loc.deliverymins ? `${loc.deliverymins} mins avg` : '—'}
</p>
</div>
<div className="space-y-1 col-span-2 border-t border-slate-100 pt-2 mt-1">
<span className="text-[10px] text-slate-455 uppercase font-bold block">Opening Hours</span> <span className="text-[10px] text-slate-455 uppercase font-bold block">Opening Hours</span>
<p className="font-bold text-slate-750 text-xs flex items-center gap-1.5 mt-0.5"> <p className="font-bold text-slate-750 text-xs flex items-center gap-1.5 mt-0.5">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> <span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
@@ -483,6 +540,138 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
</div> </div>
</div> </div>
{/* EDIT STORE MODAL */}
{showEditStoreModal && editingStore && (
<div
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-4"
onClick={(e) => { if (e.target === e.currentTarget) setShowEditStoreModal(false); }}
>
<div className="bg-white border border-[#e2e8f0] rounded-xl w-[90vw] sm:w-[450px] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200" onClick={(e) => e.stopPropagation()}>
<div className="p-4 border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center shrink-0">
<h4 className="font-bold text-[#0f172a] text-sm flex items-center gap-2">
<Store size={16} className="text-[#662582]" />
Edit Store Outlet Location
</h4>
<button
type="button"
onClick={() => setShowEditStoreModal(false)}
className="text-zinc-400 hover:text-zinc-700 bg-zinc-100 hover:bg-zinc-200 p-1.5 rounded-lg transition-colors cursor-pointer border-none"
>
</button>
</div>
<form onSubmit={handleUpdateStoreSubmit} className="flex flex-col flex-1 min-h-0">
<div className="p-4 overflow-y-auto space-y-4">
<div className="bg-purple-50/50 border border-purple-100 rounded-lg p-3 flex items-start gap-3">
<Store className="text-purple-600 mt-0.5 shrink-0" size={16} />
<div>
<h5 className="font-bold text-[#0f172a] text-xs">Update Outlet Profile</h5>
<p className="text-zinc-500 text-[11px] leading-relaxed mt-0.5">
Adjust the operational metadata for this node. The outlet will instantly be updated across the delivery fleet system.
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">OUTLET NAME (*)</label>
<input
type="text"
placeholder="e.g. Coimbatore Main"
value={editingStore.locationname}
onChange={(e) => 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
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">LOCAL ZONE AREA (*)</label>
<input
type="text"
placeholder="e.g. RS Puram"
value={editingStore.suburb}
onChange={(e) => 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
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">CITY</label>
<input
type="text"
placeholder="e.g. Coimbatore"
value={editingStore.city}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">CONTACT PHONE (*)</label>
<input
type="text"
placeholder="e.g. 9876543210"
value={editingStore.contactno}
onChange={(e) => 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
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">STATUS</label>
<select
value={editingStore.status}
onChange={(e) => setEditingStore({ ...editingStore, status: 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"
>
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="Suspended">Suspended</option>
</select>
</div>
</div>
</div>
<div className="p-4 border-t border-[#f1f5f9] flex justify-end gap-3 bg-[#f8fafc] shrink-0">
<button
type="button"
onClick={() => setShowEditStoreModal(false)}
className="px-4 py-2 border border-[#e2e8f0] rounded-lg font-semibold text-zinc-500 hover:bg-zinc-50 cursor-pointer text-xs"
>
Cancel
</button>
<button
type="submit"
disabled={updateLocationMut.isPending}
className="px-4 py-2 bg-[#662582] hover:bg-purple-800 text-white rounded-lg font-bold shadow-sm cursor-pointer border-none flex items-center gap-2 text-xs"
>
{updateLocationMut.isPending ? 'Updating...' : 'Update Outlet Profile'}
</button>
</div>
</form>
</div>
</div>
)}
<ConfirmModal
isOpen={confirmModalState.isOpen}
title="Delete Outlet"
message={
<>
Are you sure you want to delete <strong>{confirmModalState.loc?.locationname}</strong>? This action cannot be undone.
</>
}
confirmText="Delete Outlet"
onConfirm={confirmDeleteOutlet}
onCancel={() => setConfirmModalState({ isOpen: false, loc: null })}
isDestructive={true}
isLoading={deleteLocationMut.isPending}
/>
</div> </div>
); );
} }

View File

@@ -10,27 +10,28 @@ import {
Layers, Layers,
Settings, Settings,
TrendingUp, TrendingUp,
ShieldAlert ShieldAlert,
Users
} from 'lucide-react'; } from 'lucide-react';
import { NavLink, useLocation } from 'react-router-dom';
import { MainSection } from '../types'; import { MainSection } from '../types';
interface SidebarProps { interface SidebarProps {
currentSection: MainSection;
setCurrentSection: (section: MainSection) => void;
isCoimbatoreView: boolean; isCoimbatoreView: boolean;
setIsCoimbatoreView: (val: boolean) => void; setIsCoimbatoreView: (val: boolean) => void;
isOpen: boolean; isOpen: boolean;
isAdmin?: boolean; isAdmin?: boolean;
onClose: () => void;
} }
export default function Sidebar({ export default function Sidebar({
currentSection,
setCurrentSection,
isCoimbatoreView, isCoimbatoreView,
setIsCoimbatoreView, setIsCoimbatoreView,
isOpen, isOpen,
isAdmin isAdmin,
onClose
}: SidebarProps) { }: SidebarProps) {
const location = useLocation();
// Navigation elements // Navigation elements
const navItems = [ const navItems = [
{ id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard }, { id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard },
@@ -40,22 +41,30 @@ export default function Sidebar({
]; ];
return ( return (
<>
{/* Mobile Overlay */}
{isOpen && (
<div
className="md:hidden fixed inset-0 bg-black/50 z-40 backdrop-blur-sm"
onClick={onClose}
/>
)}
<aside <aside
className={`fixed left-0 top-0 h-screen bg-[#662582] border-r border-[#662582] text-white flex-col py-xl pt-16 z-40 hidden md:flex transition-all duration-300 ${ className={`fixed left-0 top-0 h-screen bg-[#662582] border-r border-[#662582] text-white flex flex-col py-xl pt-16 z-40 transition-all duration-300 ${
isOpen ? 'w-64' : 'w-20' isOpen ? 'translate-x-0 w-64' : '-translate-x-full md:translate-x-0 md:w-20'
}`} }`}
> >
{/* Main Navigation Sidebar Links */} {/* Main Navigation Sidebar Links */}
<nav className="flex-1 space-y-1 overflow-y-auto px-xs"> <nav className="flex-1 space-y-1 overflow-y-auto px-xs">
{navItems.map((item) => { {navItems.map((item) => {
const IconComponent = item.icon; const IconComponent = item.icon;
const isActive = currentSection === item.id;
return ( return (
<button <NavLink
key={item.id} key={item.id}
onClick={() => setCurrentSection(item.id)} to={`/admin/${item.id}`}
title={item.label} title={item.label}
className={`w-full flex items-center py-3 rounded-lg text-left transition-all duration-200 cursor-pointer ${ className={({ isActive }) => `w-full flex items-center py-3 rounded-lg text-left transition-all duration-200 cursor-pointer ${
isOpen ? 'gap-md px-md' : 'justify-center px-0' isOpen ? 'gap-md px-md' : 'justify-center px-0'
} ${ } ${
isActive isActive
@@ -63,13 +72,17 @@ export default function Sidebar({
: 'text-purple-200 hover:bg-white/10 hover:text-white' : 'text-purple-200 hover:bg-white/10 hover:text-white'
}`} }`}
> >
{({ isActive }) => (
<>
<IconComponent size={18} className={isActive ? 'text-white' : 'text-purple-300'} /> <IconComponent size={18} className={isActive ? 'text-white' : 'text-purple-300'} />
{isOpen && <span className="font-sans text-sm font-medium">{item.label}</span>} {isOpen && <span className="font-sans text-sm font-medium">{item.label}</span>}
</button> </>
)}
</NavLink>
); );
})} })}
</nav> </nav>
</aside> </aside>
</>
); );
} }

View File

@@ -21,7 +21,7 @@
import React, { useEffect, useMemo, useState } from 'react'; 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 { 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 { num as fnum, str as fstr, type Row, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi';
import { categoryName } from '../services/fiestaMappers'; import { categoryName } from '../services/fiestaMappers';
import { useStoreCatalogue } from '../services/storeCatalogue'; import { useStoreCatalogue } from '../services/storeCatalogue';
@@ -82,54 +82,29 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
[storeCat.items], [storeCat.items],
); );
// The user's picks: productid → request data. Persisted per store. const stockRequestsQ = useFiestaGetStockRequests({ tenantid, locationid: locationid ?? 0, pagesize: 500 });
const storageKey = `nearledaily.catalogue.request.${locationid ?? 'na'}`; const [picks, setPicks] = useState<Record<string, { qty: number; status: 'Pending' | 'Approved' | 'Rejected' | 'Cancelled' | 'Received'; requestedAt: string; resolvedAt?: string }>>({});
const [picks, setPicks] = useState<Record<string, { qty: number; status: 'Pending' | 'Approved' | 'Rejected'; requestedAt: string; resolvedAt?: string }>>(() => {
try { useEffect(() => {
const raw = localStorage.getItem(storageKey); if (stockRequestsQ.data) {
if (!raw) return {}; const map: Record<string, any> = {};
const parsed = JSON.parse(raw); stockRequestsQ.data.forEach((req: any) => {
// Migrate old format (Record<string, number>) to new format if (!map[req.productid]) {
const migrated: Record<string, any> = {}; map[req.productid] = {
for (const [k, v] of Object.entries(parsed)) { qty: req.qty,
if (typeof v === 'number') { status: req.status,
migrated[k] = { qty: v, status: 'Pending', requestedAt: new Date().toISOString() }; requestedAt: req.created,
} else { resolvedAt: req.updated,
migrated[k] = v; requestid: req.requestid
if (migrated[k].status === 'Approve') migrated[k].status = 'Approved'; };
if (migrated[k].status === 'Reject') migrated[k].status = 'Rejected';
}
}
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); }, [stockRequestsQ.data]);
}
else setPicks({});
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, [storageKey]);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(picks));
}, [picks, storageKey]);
const createRequestMutation = useFiestaCreateStockRequest(); const createRequestMutation = useFiestaCreateStockRequest();
const updateRequestMutation = useFiestaUpdateStockRequest();
const togglePick = (id: string) => { const togglePick = (id: string) => {
setNotice(false); 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`. // The request is saved to localStorage automatically via the useEffect on `picks`.
return ( return (
<div className="space-y-lg animate-in fade-in duration-300 font-sans pb-28"> <div className="animate-in fade-in duration-300 font-sans pb-28">
{/* Tabs */} {/* Tabs */}
<div className="flex items-center gap-1 bg-zinc-100/80 p-1 rounded-xl border border-zinc-200/60 w-full sm:w-auto sm:inline-flex"> <div className="flex items-center gap-1 bg-zinc-100/80 p-1 rounded-xl border border-zinc-200/60 w-full sm:w-auto sm:inline-flex">
<button <button
onClick={() => setView('catalogue')} onClick={() => setView('catalogue')}
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold transition-all ${ className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
view === 'catalogue' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800' view === 'catalogue' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
}`} }`}
> >
@@ -286,7 +261,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</button> </button>
<button <button
onClick={() => setView('inventory')} onClick={() => setView('inventory')}
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold transition-all ${ className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
view === 'inventory' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800' view === 'inventory' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
}`} }`}
> >
@@ -294,7 +269,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</button> </button>
<button <button
onClick={() => setView('requests')} onClick={() => setView('requests')}
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold transition-all ${ className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
view === 'requests' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800' view === 'requests' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
}`} }`}
> >
@@ -302,7 +277,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</button> </button>
</div> </div>
<div className="flex flex-col md:flex-row gap-6 items-start mt-6"> <div className="flex flex-col md:flex-row gap-6 items-start mt-4">
{/* Sticky Sidebar Filter */} {/* Sticky Sidebar Filter */}
{view !== 'requests' && ( {view !== 'requests' && (
<div className="w-full md:w-64 shrink-0 bg-white border border-slate-200 rounded-2xl p-5 sticky top-24 shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)] z-10 hidden md:flex flex-col gap-6"> <div className="w-full md:w-64 shrink-0 bg-white border border-slate-200 rounded-2xl p-5 sticky top-24 shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)] z-10 hidden md:flex flex-col gap-6">
@@ -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." sub="Try a different keyword or clear the filters to see the full catalogue."
/> />
) : ( ) : (
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pb-8"> <div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-4 pb-8">
{filtered.map((p) => { {filtered.map((p) => {
const stocked = inStore.has(p.id); const stocked = inStore.has(p.id);
const pick = picks[p.id]; const pick = picks[p.id];
const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Approved'; const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Approved';
return ( return (
<div key={p.id} onClick={() => 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"> <div key={p.id} onClick={() => 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 */} {/* Image Bento Block */}
<div className="w-full h-32 bg-slate-50 rounded-t-2xl relative overflow-hidden shrink-0 border-b border-slate-100 p-2"> <div className="w-full h-28 bg-zinc-50 relative overflow-hidden shrink-0">
<img src={p.image} alt={p.name} referrerPolicy="no-referrer" className="w-full h-full object-cover rounded-xl group-hover:scale-105 transition-transform duration-500 ease-out" /> <img src={p.image} alt={p.name} referrerPolicy="no-referrer" className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700 ease-in-out" />
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-t-2xl pointer-events-none" /> <div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
{stocked && ( {stocked && (
<div className="absolute top-3 right-3 bg-white/90 backdrop-blur-md px-1.5 py-1 rounded-md shadow flex items-center gap-1 z-10"> <div className="absolute top-2 right-2 bg-emerald-100/90 backdrop-blur-sm border border-emerald-200 px-1.5 py-0.5 rounded shadow-sm flex items-center gap-1 z-10">
<CheckCircle2 size={12} className="text-emerald-500" /> <CheckCircle2 size={10} className="text-emerald-700" />
<span className="text-[9px] font-black uppercase text-emerald-700 tracking-wider">In Store</span> <span className="text-[8px] font-extrabold uppercase text-emerald-800 tracking-wider">In Store</span>
</div> </div>
)} )}
<div className="absolute top-3 left-3 z-10 flex items-center gap-2"> <div className="absolute top-2 left-2 z-10 flex flex-col items-start gap-1">
<span className={`px-2 py-1 rounded-md text-[9px] font-black uppercase shadow-sm tracking-wider ${catBadgeClass(p.category)}`}> <span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shadow-sm ${
p.category.startsWith('Staples') ? 'bg-amber-100 text-amber-800' :
p.category.startsWith('Groceries') ? 'bg-emerald-100 text-emerald-800' :
p.category.startsWith('Beverages') ? 'bg-sky-100 text-sky-800' :
'bg-rose-100 text-rose-800'
}`}>
{p.category.split(' / ')[0]} {p.category.split(' / ')[0]}
</span> </span>
</div> </div>
</div> </div>
{/* Details Bento Block */} {/* Details Bento Block */}
<div className="p-3.5 flex flex-col flex-1 gap-3"> <div className="p-3 flex flex-col flex-1 gap-2">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div> <div className="flex-1 min-w-0 pb-2">
<h4 className="font-bold text-slate-900 text-xs leading-snug group-hover:text-purple-700 transition-colors line-clamp-2">{p.name}</h4> <h4 className="font-bold text-[#0f172a] text-xs leading-snug group-hover:text-[#662582] transition-colors line-clamp-2">{p.name}</h4>
<p className="text-[10px] text-slate-400 font-bold font-mono tracking-tight mt-1">{p.sku}</p> <p className="text-[10px] text-zinc-500 font-bold font-mono tracking-tight mt-1">{p.sku}</p>
</div> </div>
</div> </div>
<div className="mt-auto bg-slate-50 rounded-xl p-2.5 border border-slate-100 flex justify-between items-center"> <div className="grid grid-cols-2 gap-1.5 p-2 bg-slate-50/80 rounded-xl border border-slate-100 mt-auto">
<div> <div>
<span className="text-[9px] text-slate-400 block uppercase tracking-wider font-extrabold mb-0.5">Price</span> <span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-extrabold mb-0.5">Price</span>
<span className="font-extrabold text-slate-800 font-mono text-sm">{p.price > 0 ? `${p.price.toLocaleString('en-IN')}` : '—'}</span> <span className="font-extrabold text-slate-800 font-mono text-xs">{p.price > 0 ? `${p.price.toLocaleString('en-IN')}` : '—'}</span>
</div> </div>
<div className="text-right"> <div className="text-right">
<span className="text-[9px] text-slate-400 block uppercase tracking-wider font-extrabold mb-0.5">Unit</span> <span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-extrabold mb-0.5">Unit</span>
<span className="font-bold text-slate-600 text-[11px] bg-white px-1.5 py-0.5 rounded shadow-sm border border-slate-100">{p.unit || 'Pc'}</span> <span className="font-bold text-slate-600 text-xs bg-white px-1.5 py-0.5 rounded shadow-sm border border-slate-100">{p.unit || 'Pc'}</span>
</div> </div>
</div> </div>
@@ -545,7 +524,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<table className="w-full text-left border-collapse" style={{ minWidth: 800 }}> <table className="w-full text-left border-collapse" style={{ minWidth: 800 }}>
<thead> <thead>
<tr className="bg-slate-50/80 border-b border-slate-200"> <tr className="bg-slate-50/80 border-b border-slate-200">
{['Requested At', 'Product', 'Qty', 'Status', 'Resolved At'].map((h, i) => ( {['Requested At', 'Product', 'Qty', 'Status', 'Resolved At', ''].map((h, i) => (
<th key={i} className="px-5 py-4 text-[10px] font-black uppercase tracking-widest text-slate-500 whitespace-nowrap">{h}</th> <th key={i} className="px-5 py-4 text-[10px] font-black uppercase tracking-widest text-slate-500 whitespace-nowrap">{h}</th>
))} ))}
</tr> </tr>
@@ -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' }) : '—'} {data.resolvedAt ? new Date(data.resolvedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'}
</span> </span>
</td> </td>
<td className="px-5 py-4 whitespace-nowrap text-right">
{isApproved && (
<button
onClick={(e) => {
e.stopPropagation();
updateRequestMutation.mutate({
tenantid,
locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID,
productid: Number(pid),
requestid: data.requestid,
status: 'Received'
});
}}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold text-white bg-indigo-600 hover:bg-indigo-700 transition-colors shadow-sm"
>
<CheckCircle2 size={14} /> Mark as Received
</button>
)}
</td>
</tr> </tr>
); );
})} })}
@@ -642,38 +640,42 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
} }
/> />
) : ( ) : (
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-4">
{finalFilteredInventory.map((it, i) => ( {finalFilteredInventory.map((it, i) => (
<div key={it.id || i} onClick={() => 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"> <div key={it.id || i} onClick={() => 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 */} {/* Image Bento Block */}
<div className="w-full h-32 bg-slate-50 rounded-t-2xl relative overflow-hidden shrink-0 border-b border-slate-100 p-2"> <div className="w-full h-28 bg-zinc-50 relative overflow-hidden shrink-0">
<img src={it.image} alt={it.name} referrerPolicy="no-referrer" className="w-full h-full object-cover rounded-xl group-hover:scale-105 transition-transform duration-500 ease-out" /> <img src={it.image} alt={it.name} referrerPolicy="no-referrer" className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700 ease-in-out" />
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-t-2xl pointer-events-none" /> <div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="absolute top-3 right-3 bg-white/90 backdrop-blur-md px-1.5 py-1 rounded-md shadow flex items-center gap-1 z-10" style={{ color: it.color }}> <div className="absolute top-2 right-2 bg-white/90 backdrop-blur-md border border-slate-200 px-1.5 py-0.5 rounded shadow-sm flex items-center gap-1 z-10" style={{ color: it.color }}>
<span className="w-1.5 h-1.5 rounded-full animate-pulse" style={{ background: it.color }} /> <span className="w-1.5 h-1.5 rounded-full animate-pulse" style={{ background: it.color }} />
<span className="text-[9px] font-black uppercase tracking-wider">{it.label}</span> <span className="text-[8px] font-extrabold uppercase tracking-wider">{it.label}</span>
</div> </div>
<div className="absolute top-3 left-3 z-10 flex items-center gap-2"> <div className="absolute top-2 left-2 z-10 flex flex-col items-start gap-1">
<span className={`px-2 py-1 rounded-md text-[9px] font-black uppercase shadow-sm tracking-wider ${catBadgeClass(it.category)}`}> <span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shadow-sm ${
it.category.startsWith('Staples') ? 'bg-amber-100 text-amber-800' :
it.category.startsWith('Groceries') ? 'bg-emerald-100 text-emerald-800' :
it.category.startsWith('Beverages') ? 'bg-sky-100 text-sky-800' :
'bg-rose-100 text-rose-800'
}`}>
{it.category.split(' / ')[0]} {it.category.split(' / ')[0]}
</span> </span>
</div> </div>
</div> </div>
{/* Details Bento Block */} {/* Details Bento Block */}
<div className="p-3.5 flex flex-col flex-1 gap-3"> <div className="p-3 flex flex-col flex-1 gap-2">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div> <div className="flex-1 min-w-0 pb-2">
<h4 className="font-bold text-slate-900 text-xs leading-snug group-hover:text-purple-700 transition-colors line-clamp-2">{it.name}</h4> <h4 className="font-bold text-[#0f172a] text-xs leading-snug group-hover:text-[#662582] transition-colors line-clamp-2">{it.name}</h4>
<p className="text-[10px] text-slate-400 font-bold font-mono tracking-tight mt-1">{it.sku}</p> <p className="text-[10px] text-zinc-500 font-bold font-mono tracking-tight mt-1">{it.sku}</p>
</div> </div>
</div> </div>
<div className="mt-auto bg-slate-50 rounded-xl p-3 border border-slate-100 text-center"> <div className="mt-auto bg-slate-50/80 rounded-xl p-2.5 border border-slate-100 text-center">
<span className="text-[9px] text-slate-400 block uppercase tracking-wider font-extrabold mb-1">Live Stock</span> <span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-extrabold mb-0.5">Live Stock</span>
<span className="font-black font-mono text-xl" style={{ color: it.color }}>{(it.closing ?? 0).toLocaleString('en-IN')}</span> <span className="font-extrabold font-mono text-sm" style={{ color: it.color }}>{(it.closing ?? 0).toLocaleString('en-IN')}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -964,12 +964,10 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
{customersList.length === 0 ? ( {customersList.length === 0 ? (
<div className="bg-white/60 backdrop-blur-md border border-[#e2e8f0] rounded-3xl py-24 flex flex-col items-center gap-4 text-center px-6 shadow-sm"> <div className="bg-white/60 backdrop-blur-md border border-[#e2e8f0] rounded-3xl py-24 flex flex-col items-center gap-4 text-center px-6 shadow-sm">
<span className="flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-[#f3e8ff] to-[#e9d5ff] text-[#6d28d9] shadow-sm"><Users size={28} /></span> <span className="flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-[#f3e8ff] to-[#e9d5ff] text-[#6d28d9] shadow-sm"><Users size={28} /></span>
<div>
<p className="text-[18px] font-bold text-[#0f172a]">No customers yet</p> <p className="text-[18px] font-bold text-[#0f172a]">No customers yet</p>
<p className="text-[14px] text-zinc-500 mt-1 max-w-sm leading-relaxed"> <p className="text-[14px] text-zinc-500 max-w-[350px] leading-relaxed">
{customerSearch ? 'Nothing matches your search criteria.' : 'Customers will appear here once they place their first order.'} {customerSearch ? 'Nothing matches your search criteria.' : 'Customers will appear here once they place their first order.'}
</p> </p>
</div>
{customerSearch && ( {customerSearch && (
<button onClick={() => setCustomerSearch('')} className="mt-2 text-[13px] font-bold text-[#662582] hover:text-[#4c1d95] bg-purple-50 px-4 py-2 rounded-lg transition-colors cursor-pointer">Clear Search</button> <button onClick={() => setCustomerSearch('')} className="mt-2 text-[13px] font-bold text-[#662582] hover:text-[#4c1d95] bg-purple-50 px-4 py-2 rounded-lg transition-colors cursor-pointer">Clear Search</button>
)} )}

View File

@@ -5,6 +5,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { import {
AlertTriangle, AlertTriangle,
LayoutDashboard, LayoutDashboard,
@@ -14,7 +15,7 @@ import {
ShieldCheck, ShieldCheck,
ShoppingBag, ShoppingBag,
Truck, Truck,
Route, Route as RouteIcon,
ClipboardList, ClipboardList,
Layers, Layers,
Users, Users,
@@ -49,9 +50,8 @@ interface UserStorePageProps {
const NAV_ITEMS: UserNavItem[] = [ const NAV_ITEMS: UserNavItem[] = [
{ id: 'console', label: 'Store Console', icon: LayoutDashboard }, { id: 'console', label: 'Store Console', icon: LayoutDashboard },
{ id: 'inventory', label: 'Products', icon: Layers }, { id: 'inventory', label: 'Products', icon: Layers },
{ id: 'pos', label: 'POS Terminal', icon: ShoppingBag },
{ id: 'customers', label: 'Customers', icon: Users }, { id: 'customers', label: 'Customers', icon: Users },
{ id: 'dispatch', label: 'Dispatch', icon: Route }, { id: 'dispatch', label: 'Dispatch', icon: RouteIcon },
{ id: 'reports', label: 'Reports', icon: ClipboardList }, { id: 'reports', label: 'Reports', icon: ClipboardList },
]; ];
@@ -65,8 +65,13 @@ type StoreShape = React.ComponentProps<typeof StoreDetailView>['store'];
*/ */
export default function UserStorePage({ onLogout, user }: UserStorePageProps) { export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const [activeSection, setActiveSection] = useState<string>('console');
const [showQrModal, setShowQrModal] = useState(false); 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 // 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. // only a fallback for legacy sessions whose record predates tenantid capture.
@@ -191,26 +196,6 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
const renderSection = () => { const renderSection = () => {
if (activeSection === 'account') return renderAccount(); 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 <PosView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'dispatch') return <DispatchHubView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'reports') return <DeliveryReportsView tenantId={tenantId} locationid={resolvedLocationId || undefined} />;
// 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 <StoreCatalogView locationid={resolvedLocationId || undefined} storeName={storeName} tenantId={tenantId} />;
// The store console needs a resolved store, so gate it on the load state.
if (locationsQ.isLoading || locSummaryQ.isLoading) {
return (
<div className="flex flex-col items-center justify-center gap-3 py-24">
<div className="w-7 h-7 border-2 border-[#662582] border-t-transparent rounded-full animate-spin" />
<span className="text-xs font-semibold text-slate-500">Loading your store</span>
</div>
);
}
// Distinguish a real load failure from "no store assigned" — otherwise an // Distinguish a real load failure from "no store assigned" — otherwise an
// API outage would wrongly tell the user to contact their admin. // API outage would wrongly tell the user to contact their admin.
if (locationsQ.isError || locSummaryQ.isError) { 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 <PosView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'dispatch') return <DispatchHubView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'reports') return <DeliveryReportsView tenantId={tenantId} locationid={resolvedLocationId || undefined} />;
// 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 <StoreCatalogView locationid={resolvedLocationId || undefined} storeName={storeName} tenantId={tenantId} />;
// The store console needs a resolved store, so gate it on the load state.
if (locationsQ.isLoading || locSummaryQ.isLoading) {
return (
<div className="flex flex-col items-center justify-center gap-3 py-24">
<div className="w-7 h-7 border-2 border-[#662582] border-t-transparent rounded-full animate-spin" />
<span className="text-xs font-semibold text-slate-500">Loading your store</span>
</div>
);
}
// canManage=false hides write actions in the console. NOTE: this is a UI-only // 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 // restriction — the backend must also enforce role-based authorization on the
// write endpoints, since a hidden button doesn't stop a direct API call. // 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 ( return (
<div className="min-h-screen bg-[#f8fafc] text-[#0f172a] font-sans antialiased"> <div className="min-h-screen bg-[#f8fafc] text-[#0f172a] font-sans antialiased overflow-x-hidden">
<Header <Header
isSidebarOpen={sidebarOpen} isSidebarOpen={sidebarOpen}
onToggleSidebar={() => setSidebarOpen((s) => !s)} onToggleSidebar={() => setSidebarOpen((s) => !s)}
onHelpClick={handleHelp} onHelpClick={handleHelp}
onLogoutClick={onLogout} onLogoutClick={onLogout}
onAccountClick={() => setActiveSection('account')} onAccountClick={() => navigate('/store/account')}
onQrClick={() => setShowQrModal(true)} onQrClick={() => setShowQrModal(true)}
profile={profile} profile={profile}
storeContext={{ storeContext={{
@@ -295,35 +300,63 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
: activeSection === 'customers' : activeSection === 'customers'
? Users ? Users
: activeSection === 'dispatch' : activeSection === 'dispatch'
? Route ? RouteIcon
: activeSection === 'reports' : activeSection === 'reports'
? ClipboardList ? ClipboardList
: undefined : undefined
}} }}
/> />
<div className="flex pt-16"> <div className="flex pt-16 w-full max-w-[100vw]">
<UserStoreSidebar <UserStoreSidebar
items={NAV_ITEMS} items={NAV_ITEMS}
activeId={activeSection}
onSelect={setActiveSection}
isOpen={sidebarOpen} isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/> />
<main <main
className={`flex-1 min-w-0 transition-all duration-300 ${ className={`flex-1 min-w-0 transition-all duration-300 ${
// Dispatch is a full-bleed cockpit — fill the area exactly (no page
// padding) so it sits flush under the header. Other pages stay padded.
activeSection === 'dispatch' || activeSection === 'pos' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]' activeSection === 'dispatch' || activeSection === 'pos' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]'
} ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'}`} } ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'}`}
> >
{activeSection === 'dispatch' || activeSection === 'pos' ? ( <Routes>
<div className="w-full h-full">{renderSection()}</div> <Route index element={<Navigate to="console" replace />} />
) : ( <Route path="account" element={
<div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300"> <div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300">
{renderSection()} {renderAccount()}
</div> </div>
)} } />
<Route path="pos" element={
<div className="w-full h-full">
<PosView locationid={resolvedLocationId || undefined} tenantId={tenantId} />
</div>
} />
<Route path="dispatch" element={
<div className="w-full h-full">
<DispatchHubView locationid={resolvedLocationId || undefined} tenantId={tenantId} />
</div>
} />
<Route path="reports" element={
<div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300">
<DeliveryReportsView tenantId={tenantId} locationid={resolvedLocationId || undefined} />
</div>
} />
<Route path="inventory" element={
<div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300">
<StoreCatalogView locationid={resolvedLocationId || undefined} storeName={storeName} tenantId={tenantId} />
</div>
} />
<Route path="console" element={
<div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300">
<StoreDetailView store={buildStore()} canManage={false} only="overview" tenantId={tenantId} />
</div>
} />
<Route path="customers" element={
<div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300">
<StoreDetailView store={buildStore()} canManage={false} only="customers" tenantId={tenantId} />
</div>
} />
</Routes>
</main> </main>
</div> </div>

View File

@@ -5,6 +5,7 @@
import React from 'react'; import React from 'react';
import { type LucideIcon } from 'lucide-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`. */ /** One entry in the user store sidebar. Add to `UserStorePage`'s `NAV_ITEMS`. */
export interface UserNavItem { export interface UserNavItem {
@@ -15,10 +16,9 @@ export interface UserNavItem {
interface UserStoreSidebarProps { interface UserStoreSidebarProps {
items: UserNavItem[]; items: UserNavItem[];
activeId: string;
onSelect: (id: string) => void;
/** Collapsed → icon-only rail; expanded → labels visible. */ /** Collapsed → icon-only rail; expanded → labels visible. */
isOpen: boolean; isOpen: boolean;
onClose: () => void;
} }
/** /**
@@ -26,24 +26,32 @@ interface UserStoreSidebarProps {
* (fixed, sits under the fixed Header, collapses to an icon rail) but driven by * (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. * 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 ( return (
<>
{/* Mobile Overlay */}
{isOpen && (
<div
className="md:hidden fixed inset-0 bg-black/50 z-40 backdrop-blur-sm"
onClick={onClose}
/>
)}
<aside <aside
className={`fixed left-0 top-0 h-screen bg-[#662582] border-r border-[#662582] text-white flex-col py-xl pt-16 z-40 hidden md:flex transition-all duration-300 ${ className={`fixed left-0 top-0 h-screen bg-[#662582] border-r border-[#662582] text-white flex flex-col py-xl pt-16 z-40 transition-all duration-300 ${
isOpen ? 'w-64' : 'w-20' isOpen ? 'translate-x-0 w-64' : '-translate-x-full md:translate-x-0 md:w-20'
}`} }`}
> >
{/* Main Navigation Sidebar Links */} {/* Main Navigation Sidebar Links */}
<nav className="flex-1 space-y-1 overflow-y-auto px-xs"> <nav className="flex-1 space-y-1 overflow-y-auto px-xs">
{items.map((item) => { {items.map((item) => {
const IconComponent = item.icon; const IconComponent = item.icon;
const isActive = activeId === item.id;
return ( return (
<button <NavLink
key={item.id} key={item.id}
onClick={() => onSelect(item.id)} to={`/store/${item.id}`}
title={item.label} title={item.label}
className={`w-full flex items-center py-3 rounded-lg text-left transition-all duration-200 cursor-pointer ${ className={({ isActive }) => `w-full flex items-center py-3 rounded-lg text-left transition-all duration-200 cursor-pointer ${
isOpen ? 'gap-md px-md' : 'justify-center px-0' isOpen ? 'gap-md px-md' : 'justify-center px-0'
} ${ } ${
isActive isActive
@@ -51,12 +59,17 @@ export default function UserStoreSidebar({ items, activeId, onSelect, isOpen }:
: 'text-purple-200 hover:bg-white/10 hover:text-white' : 'text-purple-200 hover:bg-white/10 hover:text-white'
}`} }`}
> >
{({ isActive }) => (
<>
<IconComponent size={18} className={isActive ? 'text-white' : 'text-purple-300'} /> <IconComponent size={18} className={isActive ? 'text-white' : 'text-purple-300'} />
{isOpen && <span className="font-sans text-sm font-medium">{item.label}</span>} {isOpen && <span className="font-sans text-sm font-medium">{item.label}</span>}
</button> </>
)}
</NavLink>
); );
})} })}
</nav> </nav>
</aside> </aside>
</>
); );
} }

View File

@@ -33,11 +33,15 @@ const queryClient = new QueryClient({
}, },
}); });
import { BrowserRouter } from 'react-router-dom';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<CompareProvider> <CompareProvider>
<BrowserRouter>
<App /> <App />
</BrowserRouter>
</CompareProvider> </CompareProvider>
</QueryClientProvider> </QueryClientProvider>
</StrictMode>, </StrictMode>,

View File

@@ -208,11 +208,18 @@ export async function checkEmailRequest(email: string): Promise<boolean> {
/** Find a user in the tenant user list by email or authname (case-insensitive). */ /** Find a user in the tenant user list by email or authname (case-insensitive). */
export function matchTenantUser(users: Row[], email: string): Row | null { export function matchTenantUser(users: Row[], email: string): Row | null {
const target = email.toLowerCase(); const target = email.toLowerCase();
const prefix = target.split('@')[0] + '@';
return ( return (
users.find( users.find(
(u) => (u) =>
str(u[RESPONSE_FIELDS.email]).toLowerCase() === target || 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 ) ?? null
); );
} }

View File

@@ -1029,6 +1029,10 @@ export interface CreateTenantLocationInput {
latitude?: string; latitude?: string;
longitude?: string; longitude?: string;
status?: string; status?: string;
moduleid?: number;
applocationid?: number;
partnerid?: number;
roleid?: number;
} }
/** POST /tenants/createtenantlocation — Create a new store location under a tenant. */ /** POST /tenants/createtenantlocation — Create a new store location under a tenant. */
@@ -1043,6 +1047,8 @@ export interface UpdateTenantLocationInput {
contactno?: string; contactno?: string;
email?: string; email?: string;
status?: string; status?: string;
suburb?: string;
city?: string;
} }
/** PUT /tenants/updatetenantlocation — Update store location details/status. */ /** PUT /tenants/updatetenantlocation — Update store location details/status. */
@@ -1136,3 +1142,71 @@ export async function reassignDeliveries(opts: {
return fiestaSend<Row>('riders/reassigndeliveries', 'POST', opts); return fiestaSend<Row>('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<Row> {
// 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<Row>('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<Row> {
return fiestaSend<Row>('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<SalesSummaryResponse> {
// Use fiestaGet wrapper which parses standard responses
const res = await fiestaGet<{ details: SalesSummaryResponse }>('v1/web/reports/sales-summary', opts);
return res.details;
}

View File

@@ -20,6 +20,7 @@ import {
FIESTA_APPLOCATION_ID, FIESTA_APPLOCATION_ID,
FIESTA_PRIMARY_LOCATION_ID, FIESTA_PRIMARY_LOCATION_ID,
getOrderSummary, getOrderSummary,
getSalesSummary,
getRevenueSummary, getRevenueSummary,
getTimeSeries, getTimeSeries,
getLocationSummary, getLocationSummary,
@@ -61,6 +62,7 @@ import {
CreateTenantLocationInput, CreateTenantLocationInput,
updateTenantLocation, updateTenantLocation,
UpdateTenantLocationInput, UpdateTenantLocationInput,
deleteLocation,
createProductLocation, createProductLocation,
createStockRequest, createStockRequest,
getStockRequests, getStockRequests,
@@ -73,6 +75,8 @@ import {
export const fiestaKeys = { export const fiestaKeys = {
orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) => orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) =>
['fiesta', 'orderSummary', tenantid, fromdate, todate, locationid ?? 0] as const, ['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<string, unknown>) => ['fiesta', 'revenueSummary', params] as const, revenueSummary: (params: Record<string, unknown>) => ['fiesta', 'revenueSummary', params] as const,
timeSeries: (params: Record<string, unknown>) => ['fiesta', 'timeSeries', params] as const, timeSeries: (params: Record<string, unknown>) => ['fiesta', 'timeSeries', params] as const,
locationSummary: (tenantid: number) => ['fiesta', 'locationSummary', tenantid] 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) { export function useFiestaLocationSummary(tenantid: number = FIESTA_TENANT_ID) {
return useQuery({ return useQuery({
queryKey: fiestaKeys.locationSummary(tenantid), 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 }; export { FIESTA_TENANT_ID, FIESTA_APPLOCATION_ID, FIESTA_PRIMARY_LOCATION_ID };