diff --git a/Distribution/.dotnet/.workloadAdvertisingManifestSentinel10.0.300 b/Distribution/.dotnet/.workloadAdvertisingManifestSentinel10.0.300 new file mode 100644 index 0000000..e69de29 diff --git a/Distribution/.dotnet/10.0.303.aspNetCertificateSentinel b/Distribution/.dotnet/10.0.303.aspNetCertificateSentinel new file mode 100644 index 0000000..e69de29 diff --git a/Distribution/.dotnet/10.0.303.dotnetFirstUseSentinel b/Distribution/.dotnet/10.0.303.dotnetFirstUseSentinel new file mode 100644 index 0000000..e69de29 diff --git a/Distribution/.dotnet/10.0.303.toolpath.sentinel b/Distribution/.dotnet/10.0.303.toolpath.sentinel new file mode 100644 index 0000000..e69de29 diff --git a/Distribution/.dotnet/10.0.303_IsDockerContainer.dotnetUserLevelCache b/Distribution/.dotnet/10.0.303_IsDockerContainer.dotnetUserLevelCache new file mode 100644 index 0000000..c1f22fb --- /dev/null +++ b/Distribution/.dotnet/10.0.303_IsDockerContainer.dotnetUserLevelCache @@ -0,0 +1 @@ +False \ No newline at end of file diff --git a/Distribution/.dotnet/10.0.303_MachineId.dotnetUserLevelCache b/Distribution/.dotnet/10.0.303_MachineId.dotnetUserLevelCache new file mode 100644 index 0000000..1216177 --- /dev/null +++ b/Distribution/.dotnet/10.0.303_MachineId.dotnetUserLevelCache @@ -0,0 +1 @@ +bf7d0202-9a86-43b3-8323-402073a8c61a \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_3c2c0c4055d6437591042769f431029b.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_3c2c0c4055d6437591042769f431029b.trn new file mode 100644 index 0000000..3cc1bfa --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_3c2c0c4055d6437591042769f431029b.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTyW7bMBD9FYFnUaFIaqFuSeygRpDaiI226I3LKCaikAZFZUHgfy8oO0jTWy8EMeCbecvwHTn5BKhDl4fD8hlcHFGOop1LlNAaE4HLZkeqjrcdF0VZcUoZ+41yZG/hDXWo4VqXWgBmUAPmhlRYsV5jY0CUpDSEVjz1lA8j6t6RtIWBZ6uh8OMPCKP1DnXop3XGv6TZ0hZ68JMpgh9g5cYonU5kFsvt7W69wTdN/X15dXV6OcKYGhTWoA61VUW10AJzISrMdcOx7BuKe6r6UpVNRQScYNZFCE4OxWgePzkYHx1E3dGCsoJgKtAxR0ZGmWgrOcLu7ZCYzDYtUj2fy/O1e0fPEFBH8w9DT+0u9GAvnkY12cFcnM4ox8ek9BD8AUK0MPsySx0GGa132XnS9WDBRZSj9TbbDDL2Pjx9MWt70r/6D/m7NH35CnqKYK795GLCohzdSb23DrLVAnWoLrnhghjMSyUxb3qBJTCJVVUL3ipmakihXnsXrZv8NGYrF+EhzPRRh27kMKZp6212GfTeRtBxCknTrzoBN8GbScfs0/2SFKRghCXQFA9TzO7B2AA6QlK3C9O53z8QWlNCEpUpBHAx28i4z77JcZ8emMZUmgKYkqmGqB6q1rRVqxtT123da15pwspKc65Zr4yQSgmmKae94tDUKEeQws7m/Wr6viW1ajEXjcJcMo6VIBzzsq4kV9JUMiE+fczWQ8KpvjGEEoqFbGvMmWK4ZZRhTihpmGx1XaZV+rDknD1PmdxCcDD8JfnO6uBH38fsvATZFxPuJ5f+bjYvxIt1+HV2e+H1I4QspSWtS2t6Duh4PB7/AAAA//8DAAY6R+UCBAAA \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_5d637cd612c34f1d8c78e7f3b2716acb.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_5d637cd612c34f1d8c78e7f3b2716acb.trn new file mode 100644 index 0000000..6b90bcc --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_5d637cd612c34f1d8c78e7f3b2716acb.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTyU7dMBT9lcjrOHiKHWfHqCJEQYDaqjvHvuFZBPvJcRiE3r9XDg9RuuvOuvI5PsP1GwrmEVCPDrfb0ycIeUY1yn4dMcIkJhpTdUfaXnS90A2VHVdK/UY18hfwinqkhLXUasAcJGDhSIsHPlrsHGhKqCOsFYXT3M+of0PGNw6evIUmzj8gzT4G1KOfPrj4XN42vrFTXFyT4gTnYc4m2CLm5PT24u7qGp8p+f306Oj95gxzIWi8Qz3q2pZZbTUWWrdYWCWwGRXDIxtGOlDVEg3vMB8ypGCmZnYPnxpczAGy7VnDeEMw02hXI2eyKbIHM8Pd67YoWWM6KfN6Ha/H/g09QUI9qz8Cfac7sJM/sPHx0QR3MPrg5w2q0TbFLaTsYY1kdTlNJvsYqv0jx5OHkFGNrm6r68nkMabHLzndvls//w/nl8ZufIDq/AT1SFLhhCYOCzoYLNSosQFu8NBKLbqBOwmltuMYsg9LXObqPGS4T6tK1KMzM82F9Oq2Okx24zPYvKQi/ZcsQHjx+Ti6MiCoRtcpusXm6jNuShrScMILx5K3S65uwPkENkPxdJeWPf0/ECYZKYzHS0oQcnVt8qb6ZuZNueCUay0DcJQPigwjtJ3r2s4qJ2UnRytaSzhtrRCWj4PTZhg0t0ywcRCgZNFd2q3WhQJCmNCK4lGqFgupKDbcScwJjAoYpa4riM9Yq6up4IZROcIIw9p0Egs+cNxxxrEgjChuOitp2Z2PSPaNiw7V6AJSgOkvy5fepjjHMVf76qsvIdwsoXzWal2DZx/wyxr+SbQPkKpSnvGh7OW+r91ut/sDAAD//wMAfxRohPMDAAA= \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_5e3ab89dd1164922b68cd8a91afb4dd1.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_5e3ab89dd1164922b68cd8a91afb4dd1.trn new file mode 100644 index 0000000..8742053 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_5e3ab89dd1164922b68cd8a91afb4dd1.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTS0/jMBD+K5HPdXBsJ3FyA1q0FWKpoNpd7c0eT6hFcCrH4SHU/75yWsSyt73E0cgz8738Trx+QtKS8/1+9Yw+jmRBoptLnPGKsoYW9ZaVrVStbHKlmkpU8jdZEHeNb6QltQQooEEqsEIqLSupER1Qa7EpWGEZL2WaqR9G0r4T7XKLzw4wH8YfGEY3eNKSn87b4SXt1i6HfphsHoYe136M2kMCs1zdX29vN/Sqrr6vLi6ON0cc04DcWdKSqkOjrC5p3TRAJTSGqrqpqdaisx2W6Ty2OR8xeN3no338xGCH6DFCy3MuckZ5Qw4LYnXUCbbRI27f9gnJLNMy1Rdzef5t38kzBtLyxYegx3Fn0Luzp9FMrrdnx2/U42Niug/DHkN0OOsyU+17Hd3gs9Omy96hj2RBbu+zTa9jN4SnL2LdH/mv/4P+Nm1fvSJMEe3lMPlIWqLIgtxo2DmP2XqZhhXSyoZZKgujqay7hmoUmpqyaqQywlaYTL0cfHR+GqYxW/uID2GGT1pypfsRj8DPA+xcRIhTSJx+ValxEwY7Qcw+1S9YznLBRGqa4n6K2R1aFxAiJnbbMJ3m/dPCK85YgjKFgD5mGx132Tc97tIFW9sSOKIthKmZ6bBUVpUKaltVqupAlsBEUYKUIDpjG21MI4BL3hmJdUUWBJPZ2ZwvbQ2zrKip6rim0uqCKgBGrapBAGCBUHzRMbvtU5/pass447TRqqJSGEGV4IJKxlkttIKqSFH6kOTkvUyeXGPw2P9F+cZBGMahi9kpBNkXEe4mn95uNgfixXn6Oqu9HOARQ5bc0s6nmJ4MOhwOhz8AAAD//wMACwdnnQIEAAA= \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_7f218358f90d4dea89beb22e746aaa5e.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_7f218358f90d4dea89beb22e746aaa5e.trn new file mode 100644 index 0000000..54d09a2 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_7f218358f90d4dea89beb22e746aaa5e.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpSTS2/cIBSF/4rF2jgYsA3e5alGUZooGbVVdzyuExQHRoDzUDT/vcIzVZvuusHoinM5fOf6A3n1DGhEx9vt+Qv4nFCNsltLlNAeE4nbYUO6kYuRy0aInhJJfqIauSt4RyMauDGtkYAZ9IC5JR3WbDLYWpAtaS2hHS891UNC4wdSrrHw4gw0IX2DmFzwaETfnbfhtdytXGPmsNgmhhkufcrKm2Lm7Pz+anNziy+G/uv5ycn+ZIJUGjTOohH1E2hhVYcHKQ3mRmosBjlgpdhkJ+jKdy9zPkP0am6SffrjwYbsIZuRNpQ1BFOJdjWyKqtiW6sEm/dtcbJiOiv1ei2v2/EDvUBEI61/A923OzKzO3pOenGzPdqvWaWntK6TMjnEd1SjbQxbiNnByug4JXjW8/tGpaeL/ZmyTedvYJYM9jQsPqMRCVSjFdE8q+yCrw4OT2cHPqMa3dxXt7PKU4jPnyDf77ld/ge2a2UenYfq8qxoWm65JBbzVivMh0liBUxh3fWSC81sDyXz0+Cz80tYUnXpMzzE1SUa0YWaE+z9HUfz6DKYvMRi/UdfhLcx2MXk6k84LWlIwwgroiVvl1zdgXURTIbyiE1cDv3+kdCeElKsLDGCz9Wtyo/VF5UeywE72M5QANsyPRA9QSes6IQZbN+LfjK8M4S1neHcsElbqbSWzFBOJ81h6FGNoMxCtY6fsUQTbQQeqLGYc9liwcSAmSJaU2HEpOUnjtXNXHR6GiyhhGKpRI850wwLRhnmhJKBKWH6tkzabySHiHnJ/gqih/mvJ187E0MKU64OWVefINwtvvza1Zr7q/P4baV9FswTxKqkpZwvU3wIaLfb7X4BAAD//wMA4dUdUiEEAAA= \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_a1db53cd4756496796a45e4d0e060b23.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_a1db53cd4756496796a45e4d0e060b23.trn new file mode 100644 index 0000000..f7486e8 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_a1db53cd4756496796a45e4d0e060b23.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpSUTW/cNhCG/4rAsyhTJMUP3ZLYQV0j9SJeNEVvQ3JkC9FSC4raNDD2vxfUbuCmt5xIDPgOZx6+nFcS4YCkJ++Ox7sTxryQmuRxC3HGFWWWtnrPul6aXtpGWGN5K/8mNRkf8DvpiZbet94iFaiQysA66sTgaQhoW9YGxjtZcsLzQvpXAmMT8DR6bOblT0zLOEfSky9jDPO3cjeMjZ/mNTRpnvA+LhmiL8Xc3j097B939KNWf9y9f385ueBSEjRjID1RAzoToKPaWk+lt44abTUFEEMYsCvrRTbGjCnC1Czh61sNYc4Rs+95w0XDKLfkXJMAGUrZDhbcfz+WSjZMtyVeb+Ft27+SEybS8/oH0Eu6Gz+NN3k+TnjC6QhpwXTj58MBYiA1Oab5iCmPuKHZup0myOMcq+tlH6YRYyY1eXyqdhPkYU6Hn3g9XRDc/wKBEyZHetKZDkSrEWQwaMOgOuS2E6ZlKCUIzQODYZBK+KCYVQMCD156r1vOgxWK1OR5mh1Mvy8bP8uZM4FprjhKtFwqZT1DYXDoNLIASjllvA3CaC5NMAIsQGsdam+VLyb5BP5ljFjd35ZuWhmkZYHK1gGVerAUUAB1nbLSOBEUFs2HOeYxrvO6VPcx43Pa+JGefIRpwQu5d8m/jBl9XlOB+pcqwl2aw+pz9eaAljWsEUwU0ZqPa64+YxgT+owF7z6t13z/k3DFGSulrClhzNUO8kv1Gywv5UDQofMcMbTCaeYG7EwwnfE6KGXU4GXnmWg7L6UXgwsWnLPCc8kHJ1EXylgMV20ed8xLaNFT7V1HJVhDQYKhxjDDW9VxZouv3jhWj9OmG3RgnHFqwSgqhRPUCC6oZJxpAcarttj5B5Kr+aQhNXnAFHH6T8ufRp/mZR5ydXVh9ROEz2ss86PaHPltjPSfjfbt7L9iqsprwRjLV7k+0LkmB4RlTXjYxk//Snbln1T7bQpx1TBjTE2eMqS8Hq/hrmsMk+fz+fwvAAAA//8DALVuXjbCBAAA \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_ace67540371e4eae93f6d0895b617653.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_ace67540371e4eae93f6d0895b617653.trn new file mode 100644 index 0000000..e621363 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_ace67540371e4eae93f6d0895b617653.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRU207kOBD9lcivGwffEtt5A7rRIsRF063d1b75UgEvaaflODAI9b+vkoYBZrQP+xJHJVfVqXOO6xVFswPUotP9fv0EMY+oRDksIUZYg4nGVG5J3QrVCl0pzZlk9d+oROEKXlCLpHCOOg2YQwNYeFJjyzuHvQdNCfWE1WKuae5H1L4iEyoPT8FBNYx/QBrDEFGL/gzRD89zbxMq1w+Tr9LQw2Ucs4luBrNab662t3f4QjY367Oz480RxrlAFTxqUdOBVd7UWGrtsHDaYiW1xMbwzndQz+cxLcQMKZq+Gv3jBwY/5AjZtaxivCKYaXQokTfZzLCtGWH7sp+RLDSt5ni5hJff9hU9QUItK98JPZY7cX042Y12Cr0/Wb6oRPs07CHlAAsjy5B9b3IYYvHW47wPEDMq0dmcspmcg3FELdqmCd6j63gfIlwks4PnIT3eHNtWN+stKtFdGv4Bl+9MfkAt4p1TvrOOGqFtZ8HrhtQGtOmEFarznlPXOUY70khpmei48taappG1t5YxVKLbTXHXm9wNafdFr81Rgsv/ocDm9Hwdje1hzrkw/fhjpK1J95BRiyylnVNSa+FZrT0H4xQI2lhPjFIcBHgute6o4pxxRTtFrNKmVsTWIDxHJbocN9lEb/ohwvo7uGnm94PCa+MeQoTicjUDp8ILTTwW1BosZKexAW6wrRstlOW+gdnD50PMIU7DNBaXMcN9WjT7NMTtpjhN7iFkcHlKsxx/NXPiXRr85HLxYTZKKlJxMgO9nfJ+ysU38CGBywsrbyBvNz+nsIYRMkOZUoKYi1ng4nczzipTL33tGICn3EpiO6iVV7Vy0jeNajonakc4rZ0QjnfWa2Ot5o4J1lkBskElgtnbxfKcrGkceEewB6OwUKrBhmuPQVsKXBkpmPjCY3HbL3md9IQRhrVRDRbccqw441gQRiQ3yjXUfLXwpxFV1VRUVFyqhZp33t5ehVCoRFeQIvSfeLkOLg3j0OXizZXFF6Y+NVqFcd+bl1/6/QaSk456K9/vnz+Ae/zVpN+mOO/GYnH7c4j4+yLvanCPkIrZHibEeQ38yLie+hy2DwmMB389ePi56KFEOzDjlGC3bN9lIURIC4zVdLTYZbwOfR9GcEP0I2oFUxUlgr6h/c9rmlVSE3U4HA7/AgAA//8DADlZ+rXpBQAA \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_b532e6064ddb4074a63b8d295ce297d9.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_b532e6064ddb4074a63b8d295ce297d9.trn new file mode 100644 index 0000000..e9d61aa --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_b532e6064ddb4074a63b8d295ce297d9.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACoxTTW/bOBD9K8KcRYUSqQ/q1sQJNkhbB7V3u9gbP0YOEYU0KCptEPi/F5Sdtm572JOE0ejNm/fevIKTTwg9vNvvr5/RxQlyiHYpVbRqCBWkbLe07nnXc1Hwpu2qlv4HOdg7fIEeWq51qQUShg0SbmhNFBs0MQZFSUtDq5onTLmboH8FaQuDz1Zj4ad/MEzWO+jhs3XGf0mzpS306GdTBD/irZuidDqRWV1v7rbre3LTNh+vLy+PnRNOCaCwBnpoBlSdkTVphdCEa6FI14qWSMkGM2CdnsffrIsYnByLyTz+4GB8dBh1XxUVKyipBBxyMDLKRFvJCbcv+8RkkWmV6vlSXl77V3jGAH2Vvwl6hLvQo714mtRsR3Mx+t3Out2Vd4PdzUHGNDiHffB7DNHiItB6k/3gVNKCFlVTUQo5fJD6wTrMblfZekwbq6E1tKIVEbJrCGeKkY5VjHBa0ZbJTjdlInmHweH4E+oHq4Of/BCzk+7Z2Zwr76J1s5+n7NZF3J2I9nAjxwlTwxwCupjdy/iQ/SWnh8TUtKbWFaIpmWqpGrDuTFd3ujVN0zWD5rWmrKw155oNygiplGC64tWgOLYN5LDF8GSdHN/73S5p+X3e+YcVDnIe48bPYUnGZB4hh0vrZHg5dvw9oTl1fTya8Ya0BGocl4Wyk59Xo0UXz/RNaSq54YIawkslCW8HQSQySVTdCN4pZhpMsb4P3sw6/moZowxy+DS7dErZbTLri3Xka8N/ofoTN0y5ypYoCymVarQgjFEkvCkrIriQhCGTkg+a8UFADiuvHzFkyS9p3RnYepO9C/rBRtRxDmnNf5fZ6012P8o4+PB0dnbrOe7nmH1CYwPqiInFNswJ6saO+BvZt71PGvIOctgcr3HZ9n8e45V3k/8D/B8dhx7kHD0cDofDNwAAAP//AwDB5+uJuAQAAA== \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054849_f3a1d0f218a5492cacc3df523c5ff9b2.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054849_f3a1d0f218a5492cacc3df523c5ff9b2.trn new file mode 100644 index 0000000..073bb91 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054849_f3a1d0f218a5492cacc3df523c5ff9b2.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTy07cMBT9lcjrODi248TZwQyoCFEQjNqqOz9uGItgjxyHh9D8e+XMIEp3XcW68jk+j5t35NUToB6d7nbnz+DThEqU3DKihApMJK7bDWl63vVcVpIwSrr2NyqRu4I31KOWG1MbCZiBAMwtabBmg8HWgqxJbQlteOZUDxPq35FylYVnZ6AK0w+Ikwse9ein8za85LeVq8wYZlvFMMKln5LyJotZn99fbW5u8UUrvp+fnR1uTjBlgspZ1CMxgO6sanArpcHcSI27VrZYKTbYAZr8PcCcTxC9GqvJPn5qsCF5SKanFWUVwVSifYmsSirL1mqCzdsuK1liWud5uYyXY/+OniGinpYfgR7oTszoTkx4elLengzOu2mLSrSLYQcxOVgiWVyOo0ou+OL4yGp04BMq0c19cTuqNIT49CWn+4P1y/9wfq3M1nkoLtcZU3PLJbGY11ph3g4SK2AK60ZI3mlmBeTaVsEn5+cwT8WlT/AQF5WoRxdqnOCg7zSarUtg0hyz9F8iA+HVpVWweUBQiW5jsLNJxWfcNalIxQjLHHPazam4A+simATZ0ybOR/p/IFRQkhlXc4zgU3Gr0rb4pqZtvmBb2xgKYGumW6IHaDrbNZ1prRCdGAxvDGF1Yzg3bNBWKq0lM5TTQXNoRdad2y2WhSINlxKswMKQBnMuNdaSMtxo0tq2YVTorP4z1uJmzDg9tJZQQrFUncCcaYY7RhnmhJKWqc6IOu/ORyTHxnmHSnQF0cP4l+VrZ2KYwpCKY/XFlxDuZp9/1mJZgxfn8esS/jqYR4hFLk85n/fy2Nd+v9//AQAA//8DAB8jFwPzAwAA \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054850_55c23537298a4cdd905fff2a066c61c8.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054850_55c23537298a4cdd905fff2a066c61c8.trn new file mode 100644 index 0000000..7e7861e --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054850_55c23537298a4cdd905fff2a066c61c8.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpSUS2/dNhCF/4rAtSjzJT60S2IHdY3URmw0RXdDcmQL0aUuKMppYNz/XlC+gZvusiMGPMPDj4fzQhIckAzk3fF49YyprKQlZdpLgglNmaPcPLB+UHboWccsF9rwv0lLphv8TgZiVAg8OKQSNVIVWU+9HAONER1nPDLRq9oTHlcyvBCYuojPU8BuWf/EvE5LIgP5MqW4fKtnw9SFedlil5cZr9NaIIVq5vLq/ubh9o5+NPqPq/fvX3euuNYG3RTJQKKzIVghqTGipwowUDcCUAAZIcKoneOvsikVzAnmbo1f3zzEpSQsYRCdkB2jwpFTSyIUqLY9rPjw/Vid7Jgua73dy/tyeCHPmMkg2h9AX9tdhHm6KMtxxmecj5BXzBdhORwgRdKSY16OmMuEO5r9tvMMZVpScz7swzxhKqQlt/fN3QxlXPLhJ173rwiuf4HAM2ZPBiJZL7UxqKSU4LQXDKPRQWvhAzAzeoE8IOudMo4bxqUTzMleG2siMBdISx7nxcP8+7rzc4J5G5kRWqBCJ5TWLjCUFsfeIIugtdc2uCitEcpGK8EBcOfRBKdDDcknCE9Twub6kgxEcxWVY5Eq7oEqMzoKKIH6XjtlvYwaq+bDksqUtmVbm+tU8DHv/MhAPsK84iu5dzk8TQVD2XKF+peuwru8xC2U5i0BnHWsk0xW0VaOW2k+Y5wyhoIV70Pezv3+JxFaMFatbDljKs0dlKfmN1if6oZoYh8EYuTSG+ZH7G20vQ0mam31GFQfmOR9UCrI0UcH3jsZhBKjV2g0aQnWwDV7xsMYjRo9Uo9CUqUsp1b3hkouNToEiLuRN47N7Vx1fjSRCSaoA6upkl5SK2sHJpiRYIPmNc4/kJzDpyxpyQ3mhPN/rvxpCnlZl7E05xQ2P0H4vKU6P5o9kd+mRP/ZaV8u4Svmpr4WTKl+lfMDnVpyQFi3jId9/Awv5K7+k+Zhn0JCdZpz05L7Arlsx3NZ6c4xrU6n0+lfAAAA//8DAMJGy6DDBAAA \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054850_6247cb09d42c4e96ace11389a1a91bf7.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054850_6247cb09d42c4e96ace11389a1a91bf7.trn new file mode 100644 index 0000000..18211b7 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054850_6247cb09d42c4e96ace11389a1a91bf7.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpSUy27dNhCGX0XgWpR5v2iXxA5qGKmN+CAtuuNlFBPRoQ4oymlgnHcvKJ/ATXfdEQPO8J9vfs4Lyu4IaETvTqebZ8h1RT2qaQ8xwhQmFlN9IHIUZpRkoMRYQsVfqEfpDn6gEWkRAg0WMAcFWEQisedTwDGCpYRGwqRoNd3XFY0vyKUhwnMKMCzrFyhrWjIa0R8px+V7e9ulIczLFoeyzHCb1+pyaGKubx7vDvcP+KNWv9+8f/96c4W1FRhSRCOK1oRgGMdaM4mFg4Dt5Bx2jkcX3aSspa9pKVco2c3DGr+9aYhLzVDDyAbGB4KZReceRVddk+3dCocfp6Zkx3Td4v0e3o/jC3qGgkbW/wT6Wu4qzOlq3fwMzzCfXFmhXIXleHQ5oh6dynKCUhPsaJ6h+GVNtVENQTtjKI3GaSF1NFwyriMnEA3RMtponRJqYkHKKXpjtZhEZFREBUJ4jXq0w5tnV9OSu4v2D3OCXFGP7h+7h9nVaSnHX/A/vhK9/R9Am2w0Ik4kV1qD4Jw7qzwjELUKSjEfHNGTZ0ADEGmFtlQTyi0jlkuljY6O2IB69MmFp5Shu71GI1JURGFJxIJ6h4WeLHbAHfZSWWE8b42iHn1Yck15W7a1u80Vvpa9XzSij25e4bXTdyU8pQqhbqVB+FO1xIeyxC3U7s0AlAxk4IS3pK2ettp9hpgKhAoNx6Fsl3r/SWGKEdKkbKVArt2Dq0/db259aheijjIwgEi518RPIE000gQdlTJqCkIGwqkMQgQ++Wid95YHJtjkBWiFegTNb91u8TBJyi112BhtsIheYi8Nw0QqYo0xOoo2kTeO3f3c8vykI2GEYeuMwoJ7jg1nHAvCiObOBEWbm38iuZhFGNSjOygZ5n+1/CmFsqzLVLuLa7pfIHzeclsf3e6g7ynjv3fa10v4BqVr03Ipt59yGdC5R0dw61bguG+f8QU9tG/SHfYlxMSgKNU9eqyu1O10CQs1WKLE+Xw+/wMAAP//AwC8ws6OwgQAAA== \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054850_965e4b827a0a4c4d8d08cc2d43e6bc44.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054850_965e4b827a0a4c4d8d08cc2d43e6bc44.trn new file mode 100644 index 0000000..31ea21a --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054850_965e4b827a0a4c4d8d08cc2d43e6bc44.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACoyUTW/bOBCG/4owZ1GhSOrz1sQJNsi2Tmu3u9gbRY4cIgppkFS6QeD/vqDsbOp2D3sTRqN3nnlnRq9g5RNCDx/2++tntDFADtEsIUZZTWhHymZLq160fUWLsmmrqhJ/QQ7mDl+gh0YoVaoOCccaidC0IgMfFdEau5KWmrJKJE25C9C/gjSFxmejsHDhG/pgnIUe/jBWu++ptjSFmtysC+8mvLUhSqsSzOp6c7dd35Obpv50fXl5zAwYkkBhNPSgu1aplnHSNKwiQqIi3SglkZJrqeVYd115/MzYiN7KqQj68Z1Bu2gxqp4VjBeUsA4OOWgZZcIeZMDtyz6RLDatUjxfwstj/wrP6KFn+ZuhR7kLNZmLpzDMZtIXk9vtjN1dOTua3exlTIVz2Hu3Rx8NLgatN9k7U0kLWrCaUQo5fJTqwVjMblfZekodD2OjKaOMdLKtieADJy1nnAjKaMNlq+oyQd6htzj9oPrRKO+CG2N28j07q3PlbDR2dnPIbm3E3Qm0hxs5BUwJs/doY3Yv40P2mwwPiVQ3ulIMUZd8aOgwYtXqtmpVo+u6rUclKkV5WSkhFB8H3clh6Lhigo2DwKaGHLbon4yV0+9ut0te/lvv/MUKRzlPceNmv2xG0I+Qw6Wx0r8cM74G1KesT8dhvCktCzVNS0PZaZ5Xk0Ebz/yFHupSaNFRTUQ5SCKasSMSuSRDVXeiHbiuMa31vXd6VvHnkXHKIYcvs02nlN2mYX03lvxdi59Qf2DDtFfZssp1Q5EPoyZIO0GEGJFIHCnRVOMoq6qsaAc5rJx6RJ+leUljz8TWm+yDVw8mooqzT23+udReb7L7ScbR+aezs1vPcT/H7Atq41FFTBRbPyepGzPhL7BvfZ88FO1xb4J7y/2GfnDBxPSL+DwbTAZvjue62PE/r/VM853pPzcCepBzdHA4HA7/AAAA//8DAL7u70zYBAAA \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054850_dcc554cb89e74159855b8fda6d16c235.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054850_dcc554cb89e74159855b8fda6d16c235.trn new file mode 100644 index 0000000..03c8086 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054850_dcc554cb89e74159855b8fda6d16c235.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACoxUy27jOBD8FYFnUaFI6mVgDxknwQbZjI3Ys7vYGx/NmIhMGhSVTBDk3weU5YkzmMPeCLIfVdXVfENO7AEt0OXhcP0MLg4oR9FOV5TQGpMOl82WVAveLipS1C1rSMn/Qzmyd/CKFqjhSpWqA8ygBsw1qbBkRmGtoStJqQmteKopHge0eEPCFhqerYLCD39DGKx3aIH+sU77l9Rb2EL1ftRF8D3cuiEKpxKYq+vN3Xa1xjdN/fX6y5dj5ABDKlBYjRZId61SLWW4aWiFuQCFOyMEFoJpoYWpu648plkXITjRF4N++sCgfXQQ1YIWlBUE0w6950iLKBJsKQbYvh4Skkmmq3SfT9fTcfGGniGgBc1Pgh7LXajeXuwHOdpeX0QRHiGaIPbw4sMTPIse5egQ/AFCtDDps9pkH5BKUpCC1pQQlKN7oXbWQXZ7la36RFiaRhNKKO5EW2POJMMtowxzQknDRKvqMmG8g+CgP6t6b1Xwgzcxm2XPPvVZehetG/04ZLcuwmMQ8Zh3I/oBUI7Wo+ztsHsAoV+3/mFMb27sE5flGAK4mK1F3GV/imGXSOhGV4oC6JLJhkgDVavbqlWNruu2NopXirCyUpwrZqTuhJQdU5RTIzk0NcrRaoyHMZ70/34GYhvsfg/6A8F20njdi2h82N9qcNEam0ZziphM1fcTq2yuuewtuPhJZLRAdck174jGvJQC88Z0WAATWFZ1x1vJdA3J2kvvjH0cf+pEjNGcmapta9nJkgA1TctJYyTRyjDV6UpCQxvStNB2ihBZ8bKVnSIayorrxC54Par4qxUYYShHD6NLG5rdJtov1uHvNf9QZB189Mr3aWC2h83rEGH/u6kt/f7gBxsTfzOP9tsA996l1/kPmDWbky99PAuGtAjZtHtJFg2c4k7VDHNtGiwp0biuW97UXDDQHcrRlVdPELLkMGHdNJSTqVab7DKonY2g4hhS638nVsd53px25kOQ4uv1dukDXB4O+Xz7x3NXJAevNtnJAJ/+lm9Dit75IbGIYTzz0QZ6c4J15qaj8bIH0DaAitPT9pg4a/Q7h12GaI1QcUhr8JdXkzFmp80hv/Y7G8DP7GPzsyHMppgr8XaqM/2AkxX+1wf4/v7+/gMAAP//AwCxdSWE9QUAAA== \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054856_09e56a2b38574bdb8e196ea189c58142.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054856_09e56a2b38574bdb8e196ea189c58142.trn new file mode 100644 index 0000000..ac3ed9b --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054856_09e56a2b38574bdb8e196ea189c58142.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTTU/cMBD9K9Gc4+DYjvNxoyxVEWpBsGqr3ib2pFgEZ+U4fAjtf6+cXUTprRfLGvnNvPfm+RU8PhB0cLrbnT+SjzPkEN1aElxoxltW1ltedarpKl00Uopa6l+Qg7ukF+igVsaUpiUmSRNTllesl4Nh1lJb8tJyUanUE3/P0L0CusLSozNUTPN3CrObPHTww3k7PaXZ6AozTostwjTShZ8jepPIbM5vL7dX1+xzrb+df/p0eDnTnBoUzkIHtm2MaYRkdS0qppAMawdEhigtWhx025YHmPORgsexmO39Owc7RU/RdKIQsuBMtLDPwWLERLvHmbYvu8RktWmT6vlaXq/dKzxSgE7kb4Ye2p2Y0Z08zP3iRntyOCPO90npLkw7CtHR6ssqdRwxuslnx0lnoyMfIYer2+x6xDhM4eGDWbcH/Rf/IX+bpp8/k1ki2bNp8RE6UAJy+IrmznnKLjbQgS6VVS23TJU9MlUPLUOSyPpKt6rppdWUtno2+ej8Mi1zduEj/Q4rf+jgM44zHZifBnPnIpm4hCTqp07A6zDZxcTs3f6SF7yQXCbQEndLzG7IukAmUpK3Dcux3z8QoQXnicoSAvmYXWO8y77gfJce2NpWRhDZUvY17weqGttUjamt1o0ejKoMl2VllDJy6G2Lfd9KI5QYekW1hhwobTtbA9YS15wImUbNmdKVYNhzYkZz5FXbN7ZMmXj3MbsaE64fassFF6zFRjMle8kaKSRTXPBaYmP0inuz5Lh81UAOlxQ8jX9J/upMmOZpiNkxBdkHE24Wnz5vtibiyXn2vLq9mcw9hSxtC51POT0uaL/f7/8AAAD//wMAd4cViwMEAAA= \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054856_3e9580a6a36741b390d50bd290250e42.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054856_3e9580a6a36741b390d50bd290250e42.trn new file mode 100644 index 0000000..54605b1 --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054856_3e9580a6a36741b390d50bd290250e42.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTXW/bIBT9KxbPwcWAMfitbVotqrpGbbRNe8Nw3aA6EGHcD1X57xNOqq5725t1xTn3fFy/I693gFp0vt9fPYNPI1qg5OYRJVRgonDVbEjdctnWopSMM0bVb7RA7gbeUIsabkxlFGAGAjC3pMYd6w22FlRFKktozTOnfhxR+460Ky08OwNlGH9AHF3wqEU/nbfhJe/WrjRDmGwZwwArPybtTRazvHq42dyt8XUjvl9dXBxfjjBmgtJZ1CKrpDGSMtw0tMZcg8Gq1xprzay2uhdKVUeY8wmi10M52qdPDTYkD8m0tKSsJJgqdFggq5POsjs9wuZtn5XMMS3zfDGP58/2HT1DRC1dfAR6pDszgzszYbfT3p71zrtxixZoH8MeYnIwRzK7HAadXPDFacnl4MAntEB3D8V60KkPcfclp4ej9dV/OL/VZus8FKslapGouOWKWMyrTmPe9AprYBp3tVBcdswKyLVdBp+cn8I0Fiuf4DHOKlGLrvUwwlHfeTRbl8CkKWbpv0QGwqtLl8HmQd69jsFOJhWfcVekJCUjLHNMaT+l4h6si2ASZE+bOJ3o/4FQQQnJyqYYwadirdO2+KbHbX5gG1sbCmAr1jWk66GWVtbSNFYIKXrDa0NYVRvODes7q3TXKWYop33HoRFZd263mA+qMY1QnQbMOasxZ12HOwUWV5KQCqSESvdfYi3uhozr+sYSSihWWoqMY1gyyjAnlDRMSyOqfDsfkZwa5xIt0A1ED8Nflm+diWEMfSpO1RdfQriffP5Zi/kMXpzHr3P4y2CeIBa5PO18vstTX4fD4fAHAAD//wMA/WXCnPMDAAA= \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/20260917054856_5d9eff7293784d3c9fe7fbec6ab2a85b.trn b/Distribution/.dotnet/TelemetryStorageService/20260917054856_5d9eff7293784d3c9fe7fbec6ab2a85b.trn new file mode 100644 index 0000000..17e624d --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/20260917054856_5d9eff7293784d3c9fe7fbec6ab2a85b.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRUy27kIBD8FYvz4GDA2PiWpzaKsok2o+xqbzzaCYoDI8B5KJp/X+GZKDun1V4QancX1VUlfyCvngEN6HizOX8BnxNaoeyWEiVUYCJx061JO/B+aEXdU0lb2v1GK+Su4B0NqOPGNEYCZiAAc0tarNlosLUgG9JYQlteMNVDQsMHUq628OIM1CHdQ0wueDSgn87b8FreVq42U5htHcMElz5l5U0hc3Z+d7W+ucUXnfh+fnKy60yQCkDtLBqQlb0xPWW462iLuQKD5agUVopZZdUopGx2Y85niF5NdbJPXxxsyB6yGWhNWU0wlWi7QlZlVWhrlWD9vilMFpnOSn21lJfr8IFeIKKBrj4F3cEdmckdPSc9u8ke7c6s0lNazlGZHOI7WqFNDBuI2cGi0XFK8Kyn97VKTxe7nnJN529g5gz2NMw+owExgVZo0WiaVHbBV3uKp5MDn5ePOTqfnPkXVEG6uatuJ5XHEJ8PLLnbqXz5HyJfK/PoPFSXZwW74ZZLYjFvtMK8GyVWwBTWrZC818wKKAk5DT47P4c5VZc+w0NcVkIDulBTgh2/42geXQaT51j2/CXK4G0Mdja5+rKyITWpGWFlaM6bOVc/wLoIJkNZYh3nPd794QgVlJBCZY4RfK5uVX6svqn0WBpsZ1tDAWzDdEf0CG1v+7Y3nRWiF6PhrSGsaQ3nho3aSqW1ZIZyOmoOXREYSnKqJaySso522mA7tgJzYhusjBW4oVJT23R2pPRAx+pmKnN67CyhhGKpeoE50wz3jDLMCSUdU70RTcnlpyT7PPAerdAVRA/TXytfOxNDCmOu9l5XByL8mH35EVSL76/O47dF7bNgniBWxS3lfMn83qDtdrv9AwAA//8DAJ9uh65PBAAA \ No newline at end of file diff --git a/Distribution/.dotnet/TelemetryStorageService/7b4fdabac24445e18bb7e730fceaf989 b/Distribution/.dotnet/TelemetryStorageService/7b4fdabac24445e18bb7e730fceaf989 new file mode 100644 index 0000000..5dbb49f --- /dev/null +++ b/Distribution/.dotnet/TelemetryStorageService/7b4fdabac24445e18bb7e730fceaf989 @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRVTW/jNhD9KwKvlbQSKZGSgD1kHWcbLPKB2OgWvQ3JYcxGIQ2KyjYN/N8LyknX2UUPvdjSSDPz5r031Atx8IhkIGf7/foJXZxITqJdQrSivKj6ohbbqh2abmh52bGu5XX/B8mJ/YLPZCCiUapWPRYMORaNrtpCMqMKrbGvq1pXtG1STbifyPBCwJYan6zC0k+/YZisd2QgX63T/lvqDbZUo591GfyIl26K4FQCc77efNne3BYXgl+vP306vjnhlAqUVpOB6L5TqqOsEIK2RQOoit4AFABMgwbD+74+plkXMTgYy0k/fMegfXQY1UBLysqqoD055ERDhARbwoTb531CstB0nuL5El4uhxfyhIEMNH8j9Fjugxrth8dJznbUH5ZfkpN98HsM0eLCyDLkOEK03mWvPVajRRdJTj6llM2sFE4TGcgFjBO+hdfu3jq8CPCI33x4uD72La/XW5KT2+D/RBVvIe7IQJhRnTZS1dD00kjUPa9awB5MI5vOaM1qZRStTcWFkLQxrNNSAuei1VJSSnJys8luR4jGh8d3gm2OGlz+Dwk2Z6u1Azmi/nGkLYR7jGQgLW8lMtNRLjlo6FlnakDWayUZb7GTQncoBKMMVKuqrla1qFvoRdsKzmjqcjltIjgNo3e4/gvVnAgmA9mGOfW7ArWzDrPLczIQXje66StdNLWEohGmLwAZFLLlfdNJpjkmE1+AHeeAK4h470Ny//X8GZNOK++idbOfp+zSRbwPi5wn491ssrOgdjaiinNIQv3OU8nb4PWsYvbdh3VVViWrWEqa436O2R1qG1DFha9X+DebH1Mop1WVoMwhoItZkj77Faakf62FbhVF1DWTopIG2053baeE5rzjRjWtqljdqqZRzEjdg5Q9U7ShRjYoOMkJJttny6Y1XS8qo6DoIFHGelOAkrLg0vTQiKpjlXjHcHYzpjxphK5oRYseOl40TLKiY5QVTUUrwaBTvIb35j4ZsSt5WTclE91CzRtvrwvTdCQn6xB8WPk5nWLD6d2lMz57yVb+cW9HDNnHLM+upqXPZ3QYYDwNrZ9gnI/reBp9s9C77AD73RLYwvSwXGz0wx1OfnzCMC2Ba4wbfXy2uCX7mNV5tqSvdqhen0C0T7j1flQ7sMceK6/xzMH4PNljpTv42x/Bf92b5f9s2l8vFfPsJu6WybIDyckXDA7HE4dcWRX85E3MXjc3e+eZE8rP7bQf4fkn5n9BwSpTa5mk/Y7+50W+m136gGTLifDNuuKvxejnXj1gyNKigHXprPw342oeo93uAoJGfeU1/lj0kJNHhGkO+Lh8opZT02FYYJzPx2W7dFd2HO2Eyjs9kYFz3pSM0/YV7n++JxpaVqzmh8Ph8A8AAAD//wMADw4oQRAHAAA= \ No newline at end of file diff --git a/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/10.0.303.1/microsoft.net.workloads.workloadset.json b/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/10.0.303.1/microsoft.net.workloads.workloadset.json new file mode 100644 index 0000000..8e0f001 --- /dev/null +++ b/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/10.0.303.1/microsoft.net.workloads.workloadset.json @@ -0,0 +1,18 @@ +{ + "Microsoft.NET.Workload.Emscripten.Current": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Emscripten.net6": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Emscripten.net7": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Emscripten.net8": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Emscripten.net9": "10.0.111/10.0.100", + "Microsoft.NET.Sdk.Android": "36.1.69/10.0.100", + "Microsoft.NET.Sdk.iOS": "26.5.10315/10.0.100", + "Microsoft.NET.Sdk.MacCatalyst": "26.5.10315/10.0.100", + "Microsoft.NET.Sdk.macOS": "26.5.10315/10.0.100", + "Microsoft.NET.Sdk.Maui": "10.0.20/10.0.100", + "Microsoft.NET.Sdk.tvOS": "26.5.10315/10.0.100", + "Microsoft.NET.Workload.Mono.ToolChain.Current": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Mono.ToolChain.net6": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Mono.ToolChain.net7": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Mono.ToolChain.net8": "10.0.111/10.0.100", + "Microsoft.NET.Workload.Mono.ToolChain.net9": "10.0.111/10.0.100" +} diff --git a/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/AdvertisedManifestFeatureBand.txt b/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/AdvertisedManifestFeatureBand.txt new file mode 100644 index 0000000..d43750c --- /dev/null +++ b/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/AdvertisedManifestFeatureBand.txt @@ -0,0 +1 @@ +10.0.300 \ No newline at end of file diff --git a/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/workloadVersion.txt b/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/workloadVersion.txt new file mode 100644 index 0000000..bc93d3c --- /dev/null +++ b/Distribution/.dotnet/sdk-advertising/10.0.300/microsoft.net.workloads/workloadVersion.txt @@ -0,0 +1 @@ +10.0.303.1 \ No newline at end of file diff --git a/Distribution/Migrations/20260917034841_RefactorSalesToShopPOAndInvoice.cs b/Distribution/Migrations/20260917034841_RefactorSalesToShopPOAndInvoice.cs index a12b129..a312656 100644 --- a/Distribution/Migrations/20260917034841_RefactorSalesToShopPOAndInvoice.cs +++ b/Distribution/Migrations/20260917034841_RefactorSalesToShopPOAndInvoice.cs @@ -13,41 +13,16 @@ namespace Distribution.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropForeignKey( - name: "FK_DeliveryRuns_Routes_FK_RouteId", - table: "DeliveryRuns"); + DropForeignKeyIfExists(migrationBuilder, "DeliveryRuns", "FK_DeliveryRuns_Routes_FK_RouteId"); + DropForeignKeyIfExists(migrationBuilder, "DeliveryRuns", "FK_DeliveryRuns_Warehouses_FK_SourceWarehouseId"); - migrationBuilder.DropForeignKey( - name: "FK_DeliveryRuns_Warehouses_FK_SourceWarehouseId", - table: "DeliveryRuns"); - - migrationBuilder.DropForeignKey( - name: "FK_DeliveryTasks_Orders_FK_OrderId", - table: "DeliveryTasks"); - - migrationBuilder.DropForeignKey( - name: "FK_LoadingSheetItems_LoadingSheets_FK_LoadingSheetId", - table: "LoadingSheetItems"); - - migrationBuilder.DropForeignKey( - name: "FK_LoadingSheets_Warehouses_FK_SourceWarehouseId", - table: "LoadingSheets"); - - migrationBuilder.DropForeignKey( - name: "FK_Payments_Sales_FK_SaleId", - table: "Payments"); - - migrationBuilder.DropForeignKey( - name: "FK_Payments_Stores_FK_StoreId", - table: "Payments"); - - migrationBuilder.DropForeignKey( - name: "FK_SaleReturns_Orders_FK_OrderId", - table: "SaleReturns"); - - migrationBuilder.DropForeignKey( - name: "FK_SchemeRedemptions_Orders_FK_OrderId", - table: "SchemeRedemptions"); + DropForeignKeyIfExists(migrationBuilder, "DeliveryTasks", "FK_DeliveryTasks_Orders_FK_OrderId"); + DropForeignKeyIfExists(migrationBuilder, "LoadingSheetItems", "FK_LoadingSheetItems_LoadingSheets_FK_LoadingSheetId"); + DropForeignKeyIfExists(migrationBuilder, "LoadingSheets", "FK_LoadingSheets_Warehouses_FK_SourceWarehouseId"); + DropForeignKeyIfExists(migrationBuilder, "Payments", "FK_Payments_Sales_FK_SaleId"); + DropForeignKeyIfExists(migrationBuilder, "Payments", "FK_Payments_Stores_FK_StoreId"); + DropForeignKeyIfExists(migrationBuilder, "SaleReturns", "FK_SaleReturns_Orders_FK_OrderId"); + DropForeignKeyIfExists(migrationBuilder, "SchemeRedemptions", "FK_SchemeRedemptions_Orders_FK_OrderId"); migrationBuilder.DropTable( name: "OrderItems"); @@ -58,57 +33,20 @@ namespace Distribution.Migrations migrationBuilder.DropTable( name: "Sales"); - migrationBuilder.DropIndex( - name: "IX_Payments_FK_SaleId", - table: "Payments"); + DropIndexIfExists(migrationBuilder, "Payments", "IX_Payments_FK_SaleId"); + DropIndexIfExists(migrationBuilder, "LoadingSheets", "IX_LoadingSheets_FK_SourceWarehouseId"); + DropIndexIfExists(migrationBuilder, "DeliveryRuns", "IX_DeliveryRuns_FK_RouteId"); + DropIndexIfExists(migrationBuilder, "DeliveryRuns", "IX_DeliveryRuns_FK_SourceWarehouseId"); - migrationBuilder.DropIndex( - name: "IX_LoadingSheets_FK_SourceWarehouseId", - table: "LoadingSheets"); - - migrationBuilder.DropIndex( - name: "IX_DeliveryRuns_FK_RouteId", - table: "DeliveryRuns"); - - migrationBuilder.DropIndex( - name: "IX_DeliveryRuns_FK_SourceWarehouseId", - table: "DeliveryRuns"); - - migrationBuilder.DropColumn( - name: "FK_SaleId", - table: "Payments"); - - migrationBuilder.DropColumn( - name: "DispatchConfirmedAt", - table: "LoadingSheets"); - - migrationBuilder.DropColumn( - name: "DispatchConfirmedBy", - table: "LoadingSheets"); - - migrationBuilder.DropColumn( - name: "QuantityReceived", - table: "LoadingSheetItems"); - - migrationBuilder.DropColumn( - name: "QuantityResolved", - table: "LoadingSheetItems"); - - migrationBuilder.DropColumn( - name: "ActivatedAt", - table: "DeliveryRuns"); - - migrationBuilder.DropColumn( - name: "FK_RouteId", - table: "DeliveryRuns"); - - migrationBuilder.DropColumn( - name: "FK_SourceWarehouseId", - table: "DeliveryRuns"); - - migrationBuilder.DropColumn( - name: "RideType", - table: "DeliveryRuns"); + DropColumnIfExists(migrationBuilder, "Payments", "FK_SaleId"); + DropColumnIfExists(migrationBuilder, "LoadingSheets", "DispatchConfirmedAt"); + DropColumnIfExists(migrationBuilder, "LoadingSheets", "DispatchConfirmedBy"); + DropColumnIfExists(migrationBuilder, "LoadingSheetItems", "QuantityReceived"); + DropColumnIfExists(migrationBuilder, "LoadingSheetItems", "QuantityResolved"); + DropColumnIfExists(migrationBuilder, "DeliveryRuns", "ActivatedAt"); + DropColumnIfExists(migrationBuilder, "DeliveryRuns", "FK_RouteId"); + DropColumnIfExists(migrationBuilder, "DeliveryRuns", "FK_SourceWarehouseId"); + DropColumnIfExists(migrationBuilder, "DeliveryRuns", "RideType"); migrationBuilder.RenameColumn( name: "FK_OrderId", @@ -957,5 +895,66 @@ namespace Distribution.Migrations principalColumn: "Id", onDelete: ReferentialAction.Restrict); } + + private static void DropForeignKeyIfExists(MigrationBuilder migrationBuilder, string tableName, string constraintName) + { + migrationBuilder.Sql($""" + SET @sql = IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = DATABASE() + AND TABLE_NAME = '{tableName}' + AND CONSTRAINT_NAME = '{constraintName}' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + ), + 'ALTER TABLE `{tableName}` DROP FOREIGN KEY `{constraintName}`', + 'SELECT 1' + ); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + """); + } + + private static void DropIndexIfExists(MigrationBuilder migrationBuilder, string tableName, string indexName) + { + migrationBuilder.Sql($""" + SET @sql = IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{tableName}' + AND INDEX_NAME = '{indexName}' + ), + 'DROP INDEX `{indexName}` ON `{tableName}`', + 'SELECT 1' + ); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + """); + } + + private static void DropColumnIfExists(MigrationBuilder migrationBuilder, string tableName, string columnName) + { + migrationBuilder.Sql($""" + SET @sql = IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{tableName}' + AND COLUMN_NAME = '{columnName}' + ), + 'ALTER TABLE `{tableName}` DROP COLUMN `{columnName}`', + 'SELECT 1' + ); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + """); + } } } diff --git a/Distribution/appsettings.json b/Distribution/appsettings.json index 8efc9f8..d63b6f9 100644 --- a/Distribution/appsettings.json +++ b/Distribution/appsettings.json @@ -1,7 +1,7 @@ { "Urls": "http://localhost:5089", "ConnectionStrings": { - "DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution_db;User=kalana;Password=kalana@hexdive" + "DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution;User=kalana;Password=kalana@hexdive" }, "Logging": { "LogLevel": { diff --git a/Distribution/docs/refactor_docs/refactor_plan.md b/Distribution/docs/refactor_docs/refactor_plan.md index b688a38..e648038 100644 --- a/Distribution/docs/refactor_docs/refactor_plan.md +++ b/Distribution/docs/refactor_docs/refactor_plan.md @@ -684,15 +684,33 @@ appsettings.Development.json --- -## Open Items (must resolve before coding the relevant phase) +## Open Items + +### Resolved during implementation + +| # | Item | Resolution | +|---|---|---| +| 2 | Cascade when Confirmed ShopPO is cancelled and Invoice already exists | Cancel is rejected (400) if a linked Invoice exists. Invoice must be voided separately before the ShopPO can be cancelled. No automatic cascade. | +| 3 | Cascade when Confirmed ShopPO is cancelled and it's in a LoadingSheet | Cancel is rejected (400) if the ShopPO appears in any Planned LoadingSheet. Operator must remove it from the sheet first. | +| 4 | `FinalizeInvoiceRequest` payload — free issues field shape | Deferred to a later pass. v1 `POST /api/invoices/{id}/finalize` takes no body and locks the invoice as-is. | +| 5 | Who can confirm a ShopPO? | No role restriction in v1 — any user can confirm. Gating deferred until the auth layer is implemented. | + +### Still open | # | Item | Blocks | |---|---|---| -| 1 | Who holds `loading.confirm` permission? | Phase 9 — ConfirmAsync auth guard | -| 2 | Cascade when Confirmed ShopPO is cancelled and Invoice already exists | Phase 7 — CancelAsync | -| 3 | Cascade when Confirmed ShopPO is cancelled and it's in a LoadingSheet | Phase 7 — CancelAsync | -| 4 | `FinalizeInvoiceRequest` payload — free issues field shape | Phase 6 — FinalizeAsync | -| 5 | Who can confirm a ShopPO? (agent, office, either?) | Phase 7 — ConfirmAsync permission | +| 1 | Who holds `loading.confirm` permission? | Permission seeding (`loading.populate`, `loading.confirm`) — explicitly noted as **Pending** in the API reference. Must decide role before seeding. | + +### New items — identified during frontend refactor (2026-09-17) + +| # | Item | Area | +|---|---|---| +| 6 | `order-return-edit-dialog.tsx` — file not verified; client imports it but it was not reviewed during the refactor pass. Needs to be checked for old `orderId`/`OrderResponse` references. | Returns | +| 7 | `sales-visits/` components — any components in this section that interact with sales/orders need auditing against the new ShopPO/Invoice types. | Sales Visits | +| 8 | SysConfig management page not built — the backend has a `SysConfig` model and the `InvoiceCreationMode` config key is read by `ShopPOController`. A UI to view and toggle system config is missing. | Config | +| 9 | InvoiceCreationMode (ModeA vs ModeB) UI branching — the ShopPO confirm button calls `confirm` or `confirmAndCreateInvoice` depending on mode, but the mode is not currently read from the backend. The UI should fetch `GET /api/sys-config` and gate the action accordingly. | Sales / Invoice | +| 10 | Loading Sheet item population UI — `PUT /api/loading-sheets/{id}/items` exists in the API layer but no UI for populating/editing items on a Planned sheet has been built. | Dispatch | +| 11 | `PaymentMethod` enum is missing `Cheque` — per `invoice_collection.md`, the final design includes `Cash`, `Cheque`, `Credit`. The frontend only has `Cash` and `Credit`. Cheque-specific fields (`ChequeNumber`, `ChequeDate`) and `EntryMode` (`RealTime`, `DayEndManual`) are also unimplemented. | Payments | --- diff --git a/Distribution/docs/refactor_frontend_api_reference.md b/Distribution/docs/refactor_frontend_api_reference.md new file mode 100644 index 0000000..e7c1042 --- /dev/null +++ b/Distribution/docs/refactor_frontend_api_reference.md @@ -0,0 +1,978 @@ +# Frontend API Reference — ERP Distribution (Post-Refactor) + +> Last updated: 2026-09-17 +> Base URL: `https:///api` +> All responses are JSON. Enum fields are serialized as **strings** (e.g. `"Pending"`, `"ModeB"`). +> All `Guid` fields are UUID v4 strings. All `DateTime` fields are ISO 8601. + +--- + +## Table of Contents + +1. [Pagination](#pagination) +2. [Enums Reference](#enums-reference) +3. [ShopPO](#shoppo) +4. [Invoice](#invoice) +5. [Payment](#payment) +6. [Ride (DeliveryRun)](#ride-deliveryrun) +7. [Loading Sheet](#loading-sheet) +8. [Delivery Tasks](#delivery-tasks) +9. [Returns](#returns) +10. [Schemes](#schemes) +11. [System Config](#system-config) +12. [Key Relationships & Flows](#key-relationships--flows) + +--- + +## Pagination + +All list endpoints return a `PagedResponse`: + +```json +{ + "page": 1, + "pageSize": 20, + "total": 84, + "items": [ /* T[] */ ] +} +``` + +Default page size is `20`. Pass `?page=2&pageSize=50` to paginate. + +--- + +## Enums Reference + +| Enum | Values | +|------|--------| +| `ShopPOStatus` | `Pending`, `PartiallyFulfilled`, `Fulfilled`, `Cancelled` | +| `InvoiceStatus` | `Draft`, `Printed`, `Finalized` | +| `PaymentStatus` | `Pending`, `PartiallyPaid`, `Paid` | +| `PaymentMethod` | `Cash`, `Credit` | +| `RideStatus` | `Open`, `Cancelled`, `Closed` | +| `LoadingSheetStatus` | `Planned`, `Confirmed` | +| `DeliveryTaskStatus` | `Pending`, `Delivered`, `PartiallyDelivered`, `Failed` | +| `ReturnMode` | `CreditDeduction`, `ItemExchange` | +| `ReturnReasonCode` | `Expired`, `Damaged`, `OrderError`, `Other` | +| `CancelRequestType` | `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other` | +| `InvoiceCreationMode` | `ModeA`, `ModeB` | +| `SchemeRuleType` | `SlabDiscount`, `FlatPercent`, `BuyXGetY`, `FreeBundle` | + +--- + +## ShopPO + +A ShopPO is a shop-facing purchase order raised by a sales agent on behalf of a store. + +### Status Flow + +``` +Pending → PartiallyFulfilled → Fulfilled + ↘ Cancelled +``` + +### `GET /api/shop-pos` + +List all ShopPOs with optional filters. + +**Query params:** + +| Param | Type | Description | +|-------|------|-------------| +| `agentId` | `Guid?` | Filter by the agent who raised the PO | +| `routeId` | `Guid?` | Filter by the store's route | +| `shopId` | `Guid?` | Filter by store | +| `confirmed` | `bool?` | `true` = confirmed only, `false` = unconfirmed only | +| `page` | `int` | Default `1` | +| `pageSize` | `int` | Default `20` | + +**Response:** `PagedResponse` + +```json +{ + "page": 1, + "pageSize": 20, + "total": 12, + "items": [ + { + "id": "uuid", + "storeId": "uuid", + "storeName": "Colombo Mart", + "raisedById": "uuid", + "invoiceId": "uuid | null", + "totalAmount": 15000.00, + "status": "Pending", + "confirmedAt": "2026-09-17T10:00:00 | null", + "confirmedBy": "uuid | null", + "fulfilledAt": "2026-09-17T14:00:00 | null", + "items": [ + { + "id": "uuid", + "itemId": "uuid", + "itemName": "Milo 400g", + "quantity": 10, + "quantityFulfilled": 0, + "unitPrice": 850.00, + "isActive": true + } + ], + "isActive": true, + "createdAt": "2026-09-17T08:00:00" + } + ] +} +``` + +### `GET /api/shop-pos/{id}` + +**Response:** `ShopPOResponse` (same shape as above, single object) or `404`. + +--- + +## Invoice + +An Invoice is the billing document generated from a confirmed ShopPO. There is at most one Invoice per ShopPO. + +**InvoiceCreationMode** (system config) controls whether the invoice is: +- **ModeA** — auto-created when the ShopPO is confirmed +- **ModeB** — created as a separate explicit step (default) + +### Status Flow + +``` +Draft → Printed → Finalized +``` + +Once Finalized, the invoice is locked. Payments can be collected at any status. + +### `GET /api/invoices` + +**Query params:** + +| Param | Type | Description | +|-------|------|-------------| +| `agentId` | `Guid?` | Agent who raised the ShopPO | +| `routeId` | `Guid?` | Store's route | +| `shopId` | `Guid?` | Store | +| `status` | `InvoiceStatus?` | `Draft`, `Printed`, `Finalized` | +| `page` | `int` | Default `1` | +| `pageSize` | `int` | Default `20` | + +**Response:** `PagedResponse` + +```json +{ + "page": 1, + "pageSize": 20, + "total": 5, + "items": [ + { + "id": "uuid", + "shopPOId": "uuid", + "storeName": "Colombo Mart", + "status": "Draft", + "paymentStatus": "Pending", + "createdBy": "uuid", + "printedAt": null, + "printedBy": null, + "finalizedAt": null, + "finalizedBy": null, + "totalAmount": 15000.00, + "isActive": true, + "createdAt": "2026-09-17T08:30:00" + } + ] +} +``` + +### `GET /api/invoices/{id}` + +**Response:** `InvoiceResponse` (single) or `404`. + +> **Note:** The detail response from the repository also loads `Payments`. The current `InvoiceResponse` DTO does not yet expose payments — use `GET /api/invoices/{id}/payments` for that. + +--- + +## Payment + +Payments are collected against an Invoice. Multiple payments can be made on one invoice (e.g. partial cash collection). + +### `GET /api/invoices/{invoiceId}/payments` + +Returns all payments for the invoice. + +**Response:** `PaymentResponse[]` + +```json +[ + { + "id": "uuid", + "invoiceId": "uuid", + "amount": 5000.00, + "method": "Cash", + "collectedAt": "2026-09-17T15:00:00", + "collectedBy": "uuid", + "createdAt": "2026-09-17T15:00:00" + } +] +``` + +Returns `404` if the invoice does not exist. + +### `POST /api/invoices/{invoiceId}/payments` + +Record a payment against an invoice. + +**Request body:** + +```json +{ + "amount": 5000.00, + "method": "Cash", + "collectedByUserId": "uuid | null" +} +``` + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `amount` | `decimal` | Yes | Must be > 0 | +| `method` | `PaymentMethod` | Yes | `Cash` or `Credit` | +| `collectedByUserId` | `Guid?` | No | Defaults to authenticated user | + +**Response:** `PaymentResponse` or `404` if invoice not found, `400` on business rule violation. + +--- + +## Ride (DeliveryRun) + +A Ride represents a vehicle dispatch for a day. The driver moves along a route making deliveries. + +### Status Flow + +``` +Open → Closed + ↘ Cancelled +``` + +### `GET /api/rides` + +**Query params:** + +| Param | Type | Description | +|-------|------|-------------| +| `status` | `RideStatus?` | `Open`, `Cancelled`, `Closed` | +| `driverProfileId` | `Guid?` | Filter by driver | +| `vehicleId` | `Guid?` | Filter by vehicle | +| `page` | `int` | Default `1` | +| `pageSize` | `int` | Default `20` | + +**Response:** `PagedResponse` + +```json +{ + "items": [ + { + "id": "uuid", + "status": "Open", + "vehicleId": "uuid", + "vehicleNumberPlate": "WP CAB 1234", + "driverProfileId": "uuid", + "driverFullName": "Kamal Perera", + "meterstart": 12450, + "meterend": 0, + "startedAt": null, + "closedAt": null, + "createdAt": "2026-09-17T06:00:00" + } + ] +} +``` + +### `GET /api/rides/{id}` + +**Response:** `RideResponse` or `404`. + +### `POST /api/rides` + +Create a new ride. + +**Request body:** + +```json +{ + "vehicleId": "uuid", + "driverProfileId": "uuid", + "meterstart": 12450 +} +``` + +**Response:** `201 Created` with `RideResponse`. `400` on validation error. + +### `PUT /api/rides/{id}/driver` + +Re-assign the driver on an Open ride. + +**Request body:** + +```json +{ + "driverProfileId": "uuid" +} +``` + +**Response:** `RideResponse`. `404` if ride not found. `400` if ride is not Open. + +### `POST /api/rides/{id}/close` + +Close a ride and record the ending odometer. + +**Request body:** + +```json +{ + "meterend": 12890 +} +``` + +**Response:** `RideResponse`. `400` if ride is not Open. + +### `POST /api/rides/{id}/cancel` + +Cancel a ride. + +**Request body:** + +```json +{ + "typeCode": "VehicleBreakdown", + "reason": "Engine overheating" +} +``` + +| Field | Type | Notes | +|-------|------|-------| +| `typeCode` | `CancelRequestType` | `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other` | +| `reason` | `string` | Free text reason | + +**Response:** `RideResponse`. `400` if ride is not Open. + +--- + +## Loading Sheet + +A Loading Sheet records the items loaded onto a vehicle before a ride. It belongs to a Ride. + +### Status Flow + +``` +Planned → Confirmed +``` + +Items can only be edited while the sheet is `Planned`. Confirming the sheet commits stock movements (Warehouse → Vehicle). + +### `GET /api/loading-sheets` + +**Query params:** + +| Param | Type | Description | +|-------|------|-------------| +| `rideId` | `Guid?` | Filter by ride | +| `status` | `LoadingSheetStatus?` | `Planned`, `Confirmed` | +| `page` | `int` | Default `1` | +| `pageSize` | `int` | Default `20` | + +**Response:** `PagedResponse` + +```json +{ + "items": [ + { + "id": "uuid", + "fK_RideId": "uuid", + "status": "Planned", + "populatedBy": "uuid", + "populatedAt": "2026-09-17T07:00:00", + "confirmedAt": null, + "confirmedBy": null, + "createdAt": "2026-09-17T07:00:00", + "items": [ + { + "id": "uuid", + "fK_ItemId": "uuid", + "itemName": "Milo 400g", + "fK_WarehouseId": "uuid", + "warehouseName": "Colombo Warehouse", + "quantityDispatched": 24.0, + "fK_ShopPOItemId": "uuid | null" + } + ], + "orders": [ + { "shopPOId": "uuid" } + ] + } + ] +} +``` + +### `GET /api/loading-sheets/{id}` + +**Response:** `LoadingSheetResponse` or `404`. + +### `POST /api/loading-sheets` + +Create a new loading sheet for a ride. The ride must exist. Only one `Planned` sheet per ride is allowed. + +**Request body:** + +```json +{ + "fK_RideId": "uuid" +} +``` + +**Response:** `201 Created` with `LoadingSheetResponse`. `400` if ride already has a planned sheet. + +### `PUT /api/loading-sheets/{id}/items` + +Replace all items on a `Planned` loading sheet. This is a full replace — any items not included are removed. + +**Request body:** + +```json +{ + "items": [ + { + "fK_ItemId": "uuid", + "fK_WarehouseId": "uuid", + "quantity": 24.0, + "fK_ShopPOItemId": "uuid | null" + } + ] +} +``` + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `fK_ItemId` | `Guid` | Yes | The item being loaded | +| `fK_WarehouseId` | `Guid` | Yes | Source warehouse for this item | +| `quantity` | `decimal` | Yes | Units to load | +| `fK_ShopPOItemId` | `Guid?` | No | Links the load line to a specific ShopPO line item | + +**Response:** `LoadingSheetResponse`. `400` if sheet is not `Planned`. + +### `POST /api/loading-sheets/{id}/confirm` + +Confirm the loading sheet. Commits stock: moves each item from its specified warehouse to the vehicle. Sheet becomes `Confirmed` and can no longer be edited. + +**Request body:** _(none)_ + +**Response:** `LoadingSheetResponse`. `400` if already confirmed. + +--- + +## Delivery Tasks + +Delivery Tasks are the per-stop delivery jobs attached to a Ride. Each task maps to one ShopPO. + +> **Note:** Auto-generation of delivery tasks from route/ShopPO assignments is deferred and not yet implemented. Tasks must be created manually or via a future dispatch endpoint. + +### Status Flow + +``` +Pending → Delivered + ↘ PartiallyDelivered + ↘ Failed +``` + +### `GET /api/rides/{rideId}/tasks` + +List all delivery tasks for a ride, ordered by `Sequence`. + +**Response:** `DeliveryTaskResponse[]` + +```json +[ + { + "id": "uuid", + "fK_RideId": "uuid", + "fK_ShopPOId": "uuid", + "storeId": "uuid", + "storeName": "Galle Mart", + "sequence": 1, + "status": "Pending", + "customerConfirmed": false, + "confirmedAt": null, + "note": null, + "items": [ + { + "itemId": "uuid", + "itemName": "Milo 400g", + "quantity": 10, + "quantityFulfilled": 0, + "unitPrice": 850.00 + } + ], + "createdAt": "2026-09-17T08:00:00" + } +] +``` + +### `GET /api/delivery-tasks/{id}` + +**Response:** `DeliveryTaskResponse` or `404`. + +### `PUT /api/delivery-tasks/{id}/quantities` + +Set the fulfilled quantity for each item in the task. Call this before confirming. + +**Request body:** + +```json +{ + "items": [ + { "itemId": "uuid", "quantityFulfilled": 8 } + ] +} +``` + +**Response:** `DeliveryTaskResponse`. `400` if task is already closed. + +### `POST /api/delivery-tasks/{id}/confirm` + +Mark the task as Delivered or PartiallyDelivered based on quantities set. Updates ShopPO fulfillment, creates stock movement (Vehicle → External), and records payment if applicable. + +**Request body:** _(none)_ + +**Response:** `DeliveryTaskResponse`. `400` if task is not `Pending`. + +### `POST /api/delivery-tasks/{id}/fail` + +Mark the task as Failed with a reason note. + +**Request body:** + +```json +{ + "note": "Store was closed" +} +``` + +**Response:** `DeliveryTaskResponse`. `400` if task is not `Pending`. + +--- + +## Returns + +Returns are recorded against a ShopPO during the same store visit. Effects (credit deduction or item exchange) are applied immediately — no approval step. + +> **Note:** The Create endpoint (`POST /api/shop-pos/{id}/returns`) is currently stubbed (`501 Not Implemented`) while the return flow is being redesigned. `GET` and `PATCH` endpoints are fully operational. + +### `GET /api/returns` + +List all returns. + +**Response:** `SaleReturnResponse[]` + +```json +[ + { + "id": "uuid", + "shopPOId": "uuid", + "mode": "CreditDeduction", + "totalCreditAmount": 1700.00, + "orderTotalAmountAfter": 13300.00, + "isActive": true, + "createdAt": "2026-09-17T11:00:00", + "items": [ + { + "id": "uuid", + "itemId": "uuid", + "sku": "MILO400", + "itemName": "Milo 400g", + "quantity": 2.0, + "reasonCode": "Damaged", + "unitPrice": 850.00 + } + ] + } +] +``` + +### `GET /api/returns/{id}` + +**Response:** `SaleReturnResponse` or `404`. + +### `GET /api/shop-pos/{id}/returns` + +List all returns for a specific ShopPO. + +**Response:** `SaleReturnResponse[]` + +### `POST /api/shop-pos/{id}/returns` + +> **501 Not Implemented** — return flow under redesign. + +**Request body (for future reference):** + +```json +{ + "mode": "CreditDeduction", + "items": [ + { "itemId": "uuid", "quantity": 2.0, "reasonCode": "Damaged" } + ], + "createdByUserId": "uuid" +} +``` + +| `mode` | Effect | +|--------|--------| +| `CreditDeduction` | `totalCreditAmount` is netted off the ShopPO's `TotalAmount` | +| `ItemExchange` | Items are swapped; no monetary credit applied | + +### `PATCH /api/returns/{id}/reasons` + +Update the reason code on individual return line items. Only reason codes can be edited post-creation. + +**Request body:** + +```json +{ + "items": [ + { "itemId": "uuid", "reasonCode": "Expired" } + ] +} +``` + +**Response:** `SaleReturnResponse`. `409` if return is no longer active. + +--- + +## Schemes + +Schemes are promotional rules applied to ShopPOs (formerly Orders). + +### Scheme Rule Types + +| Type | Description | +|------|-------------| +| `SlabDiscount` | Tiered % discount based on order amount. Config: `SlabDiscountConfig` | +| `FlatPercent` | Flat % discount on order total. Config: `FlatPercentConfig` | +| `BuyXGetY` | Buy N of trigger item → get free payout item | +| `FreeBundle` | Buy a bundle of items → get a free payout item | + +### `GET /api/schemes` + +List all schemes (paginated). + +**Response:** `PagedResponse` + +```json +{ + "items": [ + { + "id": "uuid", + "name": "Q3 Volume Boost", + "ruleType": "SlabDiscount", + "ruleConfig": "{\"Tiers\":[{\"MinOrderAmount\":10000,\"DiscountPercent\":5}]}", + "triggerItemId": null, + "triggerItemName": null, + "triggerQty": null, + "freeItemId": null, + "freeItemName": null, + "freeQty": null, + "bundleComponents": [], + "regionId": "uuid | null", + "regionName": "Western Province | null", + "eligibleSegment": null, + "dateRangeFrom": "2026-07-01T00:00:00", + "dateRangeTo": "2026-09-30T00:00:00", + "agentLimitExempt": false, + "baselineExpectedSales": 500000.00, + "isActive": true, + "createdAt": "2026-06-15T09:00:00" + } + ] +} +``` + +### `GET /api/schemes/{id}` + +**Response:** `SchemeResponse` or `404`. + +### `POST /api/schemes` + +Create a scheme. For discount-only schemes (`SlabDiscount`, `FlatPercent`), free-goods fields can be omitted. For `BuyXGetY` / `FreeBundle`, you can supply the free-goods config inline here or set it later via `PUT .../free-goods-rule`. + +**Request body:** + +```json +{ + "name": "Q3 Volume Boost", + "ruleType": "SlabDiscount", + "ruleConfig": "{\"Tiers\":[{\"MinOrderAmount\":10000,\"DiscountPercent\":5}]}", + "regionId": null, + "eligibleSegment": null, + "dateRangeFrom": "2026-07-01T00:00:00", + "dateRangeTo": "2026-09-30T00:00:00", + "agentLimitExempt": false, + "baselineExpectedSales": 500000.00 +} +``` + +#### `ruleConfig` shapes + +**SlabDiscount:** +```json +{ "Tiers": [ { "MinOrderAmount": 10000, "DiscountPercent": 5 }, { "MinOrderAmount": 20000, "DiscountPercent": 8 } ] } +``` + +**FlatPercent:** +```json +{ "DiscountPercent": 3.5 } +``` + +**BuyXGetY / FreeBundle:** `ruleConfig` is `null` — the payout is set via `PUT .../free-goods-rule`. + +### `PUT /api/schemes/{id}` + +Update scheme metadata. Cannot change `ruleType`. + +### `DELETE /api/schemes/{id}` + +Soft-delete (sets `IsActive = false`). + +### `PUT /api/schemes/{id}/free-goods-rule` + +Set the free-goods payout for `BuyXGetY` or `FreeBundle` schemes. + +**Request body:** + +```json +{ + "triggerItemId": "uuid | null", + "triggerQty": 6, + "freeItemId": "uuid", + "freeQty": 1.0 +} +``` + +- `BuyXGetY`: `triggerItemId` + `triggerQty` required (what store must buy). +- `FreeBundle`: `triggerItemId` / `triggerQty` ignored — trigger is the bundle components. + +### `PUT /api/schemes/{id}/bundle-components/{itemId}` + +Add or update a component in a `FreeBundle` scheme. + +**Request body:** + +```json +{ "qty": 4 } +``` + +### `DELETE /api/schemes/{id}/bundle-components/{itemId}` + +Remove a bundle component. + +### `GET /api/shop-pos/{shopPOId}/scheme-eligibility` + +Check which active schemes the ShopPO qualifies for. + +**Response:** `SchemeEligibilityResponse[]` + +```json +[ + { + "schemeId": "uuid", + "schemeName": "Q3 Volume Boost", + "ruleType": "SlabDiscount", + "eligible": true, + "reason": null, + "discountAmount": 750.00, + "freeItemId": null, + "freeQty": null + } +] +``` + +### `POST /api/shop-pos/{shopPOId}/apply-scheme` + +Apply a scheme to the ShopPO (create a redemption record and apply discount/free goods). + +**Request body:** + +```json +{ "schemeId": "uuid" } +``` + +**Response:** `SchemeRedemptionResponse` + +```json +{ + "id": "uuid", + "schemeId": "uuid", + "shopPOId": "uuid", + "discountAmount": 750.00, + "freeItemId": null, + "freeItemName": null, + "freeQty": null, + "freeItemCostValue": null, + "redeemedAt": "2026-09-17T09:30:00" +} +``` + +### `GET /api/schemes/{id}/spend` + +**Response:** `SchemeSpendResponse` + +```json +{ + "schemeId": "uuid", + "totalDiscountSpend": 45000.00, + "totalFreeGoodsCostSpend": 12000.00, + "totalSpend": 57000.00, + "redemptionCount": 38 +} +``` + +### `GET /api/schemes/{id}/roi` + +**Response:** `SchemeROIResponse` + +```json +{ + "schemeId": "uuid", + "baselineExpectedSales": 500000.00, + "actualSales": 620000.00, + "incrementalSales": 120000.00, + "totalSpend": 57000.00, + "roi": 1.105 +} +``` + +--- + +## System Config + +Key-value store for system-wide configuration. All keys and values are strings. Only known keys have typed endpoints with validation. + +### `GET /api/sys-config` + +Return all config entries. + +**Response:** `SysConfigResponse[]` + +```json +[ + { + "key": "InvoiceCreationMode", + "value": "ModeB", + "updatedAt": "2026-09-17T00:00:00", + "updatedBy": null + } +] +``` + +### `GET /api/sys-config/invoice-creation-mode` + +**Response:** + +```json +{ "mode": "ModeB" } +``` + +### `PUT /api/sys-config/invoice-creation-mode` + +**Request body:** + +```json +{ "mode": "ModeA" } +``` + +| Value | Behaviour | +|-------|-----------| +| `ModeA` | Invoice is auto-created when a ShopPO is confirmed | +| `ModeB` | Invoice must be created as a separate explicit step (default) | + +**Response:** `SysConfigResponse`. `400` if value is not a valid `InvoiceCreationMode`. + +### `GET /api/sys-config/{key}` + +Generic getter by key string. `404` if key not set. + +### `PUT /api/sys-config/{key}` + +Generic raw string setter. Requires admin-level auth. + +**Request body:** + +```json +{ "value": "SomeValue" } +``` + +**Response:** `SysConfigResponse`. `400` if value fails validation for a known key. + +--- + +## Key Relationships & Flows + +### Core Entity Chain + +``` +Store ──< ShopPO ──< ShopPOItem + │ + └──► Invoice ──< Payment + │ + └──► SaleReturn ──< SaleReturnItem + │ + └──► SchemeRedemption + +Ride (DeliveryRun) ──► LoadingSheet ──< LoadingSheetItem + └──< DeliveryTask ──► ShopPO +``` + +### ShopPO → Invoice Dependency + +- `ShopPO.invoiceId` is `null` until an invoice is created. +- In **ModeA**, the invoice is created automatically when the ShopPO is confirmed. +- In **ModeB**, the invoice must be created via a dedicated endpoint (not yet exposed — planned). +- An invoice can only exist for a confirmed ShopPO. + +### Payment → Invoice Dependency + +- Payments are always attached to an **Invoice**, never directly to a ShopPO. +- `Invoice.paymentStatus` is computed: + - `Pending` — no payments + - `PartiallyPaid` — payments collected but total < invoice amount + - `Paid` — fully settled + +### Loading Sheet → Stock + +- Confirming a loading sheet deducts each item from its **source warehouse** (`fK_WarehouseId` per line) and credits the **vehicle** stock location. +- Each loading sheet line can optionally link to a `fK_ShopPOItemId` for traceability. + +### Delivery Task → ShopPO Fulfillment + +- Confirming a delivery task updates `ShopPOItem.quantityFulfilled` and transitions the ShopPO status: + - All items fully fulfilled → `Fulfilled` + - Some items partially fulfilled → `PartiallyFulfilled` + +### Returns → ShopPO + +- Returns always reference a `shopPOId`. +- `CreditDeduction` mode: `SaleReturn.totalCreditAmount` is netted off `ShopPO.totalAmount` immediately. +- `ItemExchange` mode: no monetary impact; items are swapped in stock. +- Only the `reasonCode` on each return line can be edited after creation. + +--- + +## What Is Not Yet Implemented + +| Feature | Status | Notes | +|---------|--------|-------| +| ShopPO confirm endpoint | Pending | `PUT /api/shop-pos/{id}/confirm` — triggers InvoiceCreationMode check | +| Invoice create (ModeB) | Pending | `POST /api/invoices` with `{ shopPOId }` | +| Invoice print | Pending | `POST /api/invoices/{id}/print` | +| Invoice finalize | Pending | `POST /api/invoices/{id}/finalize` | +| Delivery task auto-generation | Deferred | Route-based dispatch not yet implemented | +| Returns create | Redesign | `POST /api/shop-pos/{id}/returns` returns `501` | +| Permission seeding | Pending | `loading.populate`, `loading.confirm` scopes | diff --git a/distribution_fe/app/(dashboard)/operations/deliveries/_components/delivery-tasks-client.tsx b/distribution_fe/app/(dashboard)/operations/deliveries/_components/delivery-tasks-client.tsx index 64014af..648742d 100644 --- a/distribution_fe/app/(dashboard)/operations/deliveries/_components/delivery-tasks-client.tsx +++ b/distribution_fe/app/(dashboard)/operations/deliveries/_components/delivery-tasks-client.tsx @@ -51,7 +51,7 @@ export function DeliveryTasksClient() { const fetchRides = useCallback(async () => { try { - const res = await ridesApi.list({ page: 1, pageSize: 200, rideType: 'Delivery' }) + const res = await ridesApi.list({ page: 1, pageSize: 200 }) const items = getPaginatedItems(res) setRides(items) setSelectedRideId((current) => current || items[0]?.id || '') @@ -188,7 +188,7 @@ export function DeliveryTasksClient() { 0}>Select a ride... {rides.map((ride) => ( - {ride.routeName} - {ride.vehicleNumberPlate} + {ride.vehicleNumberPlate} — {ride.driverFullName} ))} diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-create-dialog.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-create-dialog.tsx new file mode 100644 index 0000000..b0031bc --- /dev/null +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-create-dialog.tsx @@ -0,0 +1,113 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useForm, zodResolver, z } from '@/lib/form' +import { FormField } from '@/components/shared/form-field' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { FieldGroup } from '@/components/ui/field' +import { toast } from '@/components/ui/toast' +import { loadingSheets as loadingSheetsApi, rides as ridesApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { RideResponse } from '@/types/api' + +const schema = z.object({ + fK_RideId: z.string().min(1, 'Ride is required'), +}) + +type FormValues = z.infer + +interface LoadingSheetCreateDialogProps { + open: boolean + onClose: () => void + onSuccess: () => void +} + +export function LoadingSheetCreateDialog({ open, onClose, onSuccess }: LoadingSheetCreateDialogProps) { + const [ridesList, setRidesList] = useState([]) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { fK_RideId: '' }, + }) + + useEffect(() => { + if (!open) return + form.reset({ fK_RideId: '' }) + ridesApi.list({ status: 'Open', pageSize: 200 }) + .then((res) => setRidesList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load rides', description: getApiError(err), type: 'error' })) + }, [open]) + + async function onSubmit(values: FormValues) { + try { + await loadingSheetsApi.create(values) + toast.add({ title: 'Loading sheet created', type: 'success' }) + onSuccess() + onClose() + } catch (err) { + toast.add({ title: 'Failed to create loading sheet', description: getApiError(err), type: 'error' }) + } + } + + return ( + !v && onClose()}> + + + New Loading Sheet + + Create a loading sheet for an open ride. Only one planned sheet per ride is allowed. + + + +
+ + ( + + )} + /> + + + + + + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-view-sheet.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-view-sheet.tsx index 8ab855d..227f6ad 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-view-sheet.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-view-sheet.tsx @@ -11,11 +11,8 @@ import { Separator } from '@/components/ui/separator' import type { LoadingSheetResponse, LoadingSheetStatus } from '@/types/api' const LS_STATUS_VARIANT: Record = { - Pending: 'secondary', - DispatchConfirmed: 'default', - ReceiptConfirmed: 'default', - DiscrepancyFlagged: 'destructive', - Resolved: 'secondary', + Planned: 'secondary', + Confirmed: 'default', } const fmtDateTime = (v: string | null) => @@ -25,6 +22,8 @@ const fmtDateTime = (v: string | null) => }) : '—' +const truncId = (id: string) => id.substring(0, 8) + interface LoadingSheetViewSheetProps { open: boolean onClose: () => void @@ -51,13 +50,12 @@ export function LoadingSheetViewSheet({ open, onClose, sheet }: LoadingSheetView
- {sheet.id.substring(0, 8)}} /> - + {truncId(sheet.id)}} /> + {truncId(sheet.fK_RideId)}} /> {sheet.status}} /> - - - - + + + 0 ? sheet.orders.map(o => truncId(o.shopPOId)).join(', ') : '—'} />
@@ -70,15 +68,14 @@ export function LoadingSheetViewSheet({ open, onClose, sheet }: LoadingSheetView Item - Dispatched - Received - Resolved + Warehouse + Qty {sheet.items.length === 0 ? ( - + No items on this sheet. @@ -86,9 +83,8 @@ export function LoadingSheetViewSheet({ open, onClose, sheet }: LoadingSheetView sheet.items.map((item) => ( {item.itemName} + {item.warehouseName} {item.quantityDispatched} - {item.quantityReceived} - {item.quantityResolved} )) )} diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx index 36bde8a..25acbc0 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx @@ -1,14 +1,19 @@ 'use client' import { useState, useEffect, useCallback, useMemo } from 'react' +import { HugeiconsIcon } from '@hugeicons/react' +import { Add01Icon } from '@hugeicons/core-free-icons' + import { DataTable } from '@/components/shared/data-table' import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/toast' import { loadingSheets as loadingSheetsApi, getApiError, getPaginatedItems } from '@/lib/api/index' import type { LoadingSheetResponse, LoadingSheetStatus } from '@/types/api' import { LoadingSheetViewSheet } from './loading-sheet-view-sheet' +import { LoadingSheetCreateDialog } from './loading-sheet-create-dialog' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -16,11 +21,8 @@ const fmtDate = (v: string) => const truncId = (id: string) => id.substring(0, 8) const LS_STATUS_VARIANT: Record = { - Pending: 'secondary', - DispatchConfirmed: 'default', - ReceiptConfirmed: 'default', - DiscrepancyFlagged: 'destructive', - Resolved: 'secondary', + Planned: 'secondary', + Confirmed: 'default', } export function LoadingSheetsClient() { @@ -30,6 +32,7 @@ export function LoadingSheetsClient() { const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) + const [createOpen, setCreateOpen] = useState(false) const [viewOpen, setViewOpen] = useState(false) const [selectedSheet, setSelectedSheet] = useState(null) @@ -55,23 +58,13 @@ export function LoadingSheetsClient() { setViewOpen(true) } - async function handleConfirmDispatch(row: LoadingSheetResponse) { + async function handleConfirm(row: LoadingSheetResponse) { try { - await loadingSheetsApi.confirmDispatch(row.id) - toast.add({ title: 'Dispatch confirmed', type: 'success' }) + await loadingSheetsApi.confirm(row.id) + toast.add({ title: 'Loading sheet confirmed — stock committed to vehicle', type: 'success' }) fetchData(page, pageSize) } catch (err) { - toast.add({ title: 'Failed to confirm dispatch', description: getApiError(err), type: 'error' }) - } - } - - async function handleConfirmReceipt(row: LoadingSheetResponse) { - try { - await loadingSheetsApi.confirmReceipt(row.id, []) - toast.add({ title: 'Receipt confirmed', type: 'success' }) - fetchData(page, pageSize) - } catch (err) { - toast.add({ title: 'Failed to confirm receipt', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to confirm loading sheet', description: getApiError(err), type: 'error' }) } } @@ -92,33 +85,45 @@ export function LoadingSheetsClient() { ), }, { - key: 'warehouseName', - title: 'Warehouse', - fields: [{ key: 'warehouseName', primary: true }], - minWidth: 180, + key: 'rideId', + title: 'Ride', + fields: ['fK_RideId'], + width: 140, + cellRender: (row) => ( + {truncId(row.fK_RideId)} + ), }, { key: 'status', title: 'Status', fields: ['status'], - width: 180, + width: 140, cellRender: (row) => ( {row.status} ), }, + { + key: 'orders', + title: 'POs', + fields: ['orders'], + width: 80, + cellRender: (row) => ( + {row.orders.length} + ), + }, { key: 'itemCount', title: 'Items', fields: ['items'], - width: 120, + width: 80, cellRender: (row) => ( - {row.items.length} items + {row.items.length} ), }, { - key: 'createdAt', - title: 'Created', - fields: [{ key: 'createdAt', transform: (v) => fmtDate(v as string) }], + key: 'populatedAt', + title: 'Populated', + fields: [{ key: 'populatedAt', transform: (v) => fmtDate(v as string) }], width: 140, }, ], @@ -131,15 +136,9 @@ export function LoadingSheetsClient() { }, { action: 'confirm', - label: 'Confirm dispatch', - onClick: handleConfirmDispatch, - visible: (target) => target.status === 'Pending', - }, - { - action: 'confirm', - label: 'Confirm receipt', - onClick: handleConfirmReceipt, - visible: (target) => target.status === 'DispatchConfirmed', + label: 'Confirm & commit stock', + onClick: handleConfirm, + visible: (target) => target.status === 'Planned', }, ], }, @@ -151,9 +150,13 @@ export function LoadingSheetsClient() {

Loading Sheets

- Track goods loaded onto a vehicle before dispatch + Items loaded onto a vehicle before a delivery run

+ + setCreateOpen(false)} + onSuccess={() => fetchData(page, pageSize)} + /> + setViewOpen(false)} diff --git a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-cancel-dialog.tsx b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-cancel-dialog.tsx index 8bfcf52..2a536ee 100644 --- a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-cancel-dialog.tsx +++ b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-cancel-dialog.tsx @@ -57,12 +57,12 @@ export function RideCancelDialog({ open, onClose, onSuccess, ride }: RideCancelD async function onSubmit(values: FormValues) { try { - await ridesApi.cancelRequests.raise(ride!.id, values) - toast.add({ title: 'Cancel request raised', type: 'success' }) + await ridesApi.cancel(ride!.id, values) + toast.add({ title: 'Ride cancelled', type: 'success' }) onSuccess() onClose() } catch (err) { - toast.add({ title: 'Failed to raise cancel request', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to cancel ride', description: getApiError(err), type: 'error' }) } } @@ -72,7 +72,7 @@ export function RideCancelDialog({ open, onClose, onSuccess, ride }: RideCancelD Cancel Ride - Raises a cancel request for this ride, subject to supervisor approval. + Cancelling this ride is immediate and cannot be undone. @@ -122,7 +122,7 @@ export function RideCancelDialog({ open, onClose, onSuccess, ride }: RideCancelD Back diff --git a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-edit-driver-sheet.tsx b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-edit-driver-sheet.tsx index 59c1d4f..6c17d8f 100644 --- a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-edit-driver-sheet.tsx +++ b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-edit-driver-sheet.tsx @@ -72,7 +72,7 @@ export function RideEditDriverSheet({ open, onClose, onSuccess, ride }: RideEdit Edit Ride Driver - Reassign the driver for this ride. Only available while the ride is in Draft status. + Reassign the driver for this ride. Only available while the ride is Open. diff --git a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx index 840f79c..bb5412b 100644 --- a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx +++ b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx @@ -26,19 +26,14 @@ import { rides as ridesApi, vehicles as vehiclesApi, drivers as driversApi, - routes as routesApi, - warehouses as warehousesApi, getApiError, getPaginatedItems, } from '@/lib/api/index' -import type { VehicleResponse, DriverProfileResponse, RouteResponse, WarehouseResponse } from '@/types/api' +import type { VehicleResponse, DriverProfileResponse } from '@/types/api' const schema = z.object({ - rideType: z.enum(['Delivery', 'Sale']), vehicleId: z.string().min(1, 'Vehicle is required'), driverProfileId: z.string().min(1, 'Driver is required'), - routeId: z.string().min(1, 'Route is required'), - sourceWarehouseId: z.string().min(1, 'Warehouse is required'), meterstart: z.number().min(0, 'Meter start must be 0 or more'), }) @@ -50,21 +45,15 @@ interface RideSheetProps { onSuccess: () => void } - export function RideSheet({ open, onClose, onSuccess }: RideSheetProps) { const [vehiclesList, setVehiclesList] = useState([]) const [driversList, setDriversList] = useState([]) - const [routesList, setRoutesList] = useState([]) - const [warehousesList, setWarehousesList] = useState([]) const form = useForm({ resolver: zodResolver(schema), defaultValues: { - rideType: 'Delivery', vehicleId: '', driverProfileId: '', - routeId: '', - sourceWarehouseId: '', meterstart: 0, }, }) @@ -73,26 +62,18 @@ export function RideSheet({ open, onClose, onSuccess }: RideSheetProps) { if (!open) return form.reset({ - rideType: 'Delivery', vehicleId: '', driverProfileId: '', - routeId: '', - sourceWarehouseId: '', meterstart: 0, }) - // available=true tells backend to exclude vehicles already on an open ride Promise.all([ vehiclesApi.list({ available: true, isActive: true, pageSize: 200 }), driversApi.list({ pageSize: 200 }), - routesApi.list({ pageSize: 200 }), - warehousesApi.list({ pageSize: 200 }), ]) - .then(([v, d, r, w]) => { + .then(([v, d]) => { setVehiclesList(getPaginatedItems(v)) setDriversList(getPaginatedItems(d)) - setRoutesList(getPaginatedItems(r)) - setWarehousesList(getPaginatedItems(w)) }) .catch((err) => { toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) @@ -115,30 +96,11 @@ export function RideSheet({ open, onClose, onSuccess }: RideSheetProps) { Create Ride - Start a new delivery or sales run. + Start a new delivery run for a driver and vehicle.
- ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - = { - Draft: 'secondary', - Open: 'secondary', - Active: 'default', - CancelRequested: 'destructive', + Open: 'default', Cancelled: 'destructive', Closed: 'secondary', } -const RIDE_TYPE_VARIANT: Record = { - Delivery: 'default', - Sale: 'secondary', -} - const fmtDateTime = (v: string | null) => v ? new Date(v).toLocaleString('en-GB', { @@ -58,14 +50,10 @@ export function RideViewSheet({ open, onClose, ride }: RideViewSheetProps) {
- - - {ride.rideType}} /> {ride.status}} /> - - + + -
diff --git a/distribution_fe/app/(dashboard)/operations/rides/_components/rides-client.tsx b/distribution_fe/app/(dashboard)/operations/rides/_components/rides-client.tsx index 8e58ea2..0fe1f35 100644 --- a/distribution_fe/app/(dashboard)/operations/rides/_components/rides-client.tsx +++ b/distribution_fe/app/(dashboard)/operations/rides/_components/rides-client.tsx @@ -10,7 +10,7 @@ import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/toast' import { rides as ridesApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { RideResponse, RideStatus, RideType } from '@/types/api' +import type { RideResponse, RideStatus } from '@/types/api' import { RideSheet } from './ride-sheet' import { RideViewSheet } from './ride-view-sheet' @@ -22,19 +22,11 @@ const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) const RIDE_STATUS_VARIANT: Record = { - Draft: 'secondary', - Open: 'secondary', - Active: 'default', - CancelRequested: 'destructive', + Open: 'default', Cancelled: 'destructive', Closed: 'secondary', } -const RIDE_TYPE_VARIANT: Record = { - Delivery: 'default', - Sale: 'secondary', -} - export function RidesClient() { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) @@ -81,16 +73,6 @@ export function RidesClient() { setCancelOpen(true) } - async function handleActivate(row: RideResponse) { - try { - await ridesApi.activate(row.id) - toast.add({ title: 'Ride activated', type: 'success' }) - fetchData(page, pageSize) - } catch (err) { - toast.add({ title: 'Failed to activate ride', description: getApiError(err), type: 'error' }) - } - } - function handleClose(row: RideResponse) { setSelectedRide(row) setCloseOpen(true) @@ -115,30 +97,29 @@ export function RidesClient() { ), }, - { - key: 'routeName', - title: 'Route', - fields: [{ key: 'routeName', primary: true }], - minWidth: 160, - }, - { - key: 'rideType', - title: 'Type', - fields: ['rideType'], - width: 120, - cellRender: (row) => ( - {row.rideType} - ), - }, { key: 'status', title: 'Status', fields: ['status'], - width: 160, + width: 140, cellRender: (row) => ( {row.status} ), }, + { + key: 'meter', + title: 'Meter', + fields: ['meterstart', 'meterend'], + width: 160, + cellRender: (row) => ( +
+

{row.meterstart} km

+ {row.meterend ? ( +

→ {row.meterend} km

+ ) : null} +
+ ), + }, { key: 'createdAt', title: 'Created', @@ -157,25 +138,19 @@ export function RidesClient() { action: 'edit', label: 'Edit driver', onClick: handleEditDriver, - visible: (target) => target.status === 'Draft', - }, - { - action: 'activate', - label: 'Activate ride', - onClick: handleActivate, visible: (target) => target.status === 'Open', }, { action: 'close', label: 'Close ride', onClick: handleClose, - visible: (target) => target.status === 'Active', + visible: (target) => target.status === 'Open', }, { action: 'cancel', label: 'Cancel ride', onClick: handleCancel, - visible: (target) => target.status === 'Active', + visible: (target) => target.status === 'Open', }, ], }, @@ -192,7 +167,7 @@ export function RidesClient() { diff --git a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx index 05bfb64..b1693bd 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx @@ -27,14 +27,13 @@ import { HugeiconsIcon } from '@hugeicons/react' import { Add01Icon, Delete02Icon } from '@hugeicons/core-free-icons' import { saleReturns as saleReturnsApi, - orders as ordersApi, - stores as storesApi, + shopPos as shopPosApi, items as itemsApi, users as usersApi, getApiError, getPaginatedItems, } from '@/lib/api/index' -import type { OrderResponse, ItemSummaryResponse, UserResponse, ReturnReasonCode } from '@/types/api' +import type { ShopPOResponse, ItemSummaryResponse, UserResponse, ReturnReasonCode } from '@/types/api' const REASON_CODES: ReturnReasonCode[] = ['Expired', 'Damaged', 'OrderError', 'Other'] @@ -45,7 +44,7 @@ const returnItemSchema = z.object({ }) const schema = z.object({ - orderId: z.string().min(1, 'Order is required'), + shopPOId: z.string().min(1, 'Shop PO is required'), mode: z.enum(['CreditDeduction', 'ItemExchange']), createdByUserId: z.string().min(1, 'Created by is required'), items: z.array(returnItemSchema).min(1, 'At least one item is required'), @@ -64,58 +63,55 @@ function defaultItem() { } export function OrderReturnDialog({ open, onClose, onSuccess }: OrderReturnDialogProps) { - const [ordersList, setOrdersList] = useState([]) - const [storeNameById, setStoreNameById] = useState>(new Map()) + const [shopPOList, setShopPOList] = useState([]) const [itemsList, setItemsList] = useState([]) const [usersList, setUsersList] = useState([]) - const [selectedOrder, setSelectedOrder] = useState(null) + const [selectedPO, setSelectedPO] = useState(null) const form = useForm({ resolver: zodResolver(schema), - defaultValues: { orderId: '', mode: 'CreditDeduction', createdByUserId: '', items: [defaultItem()] }, + defaultValues: { shopPOId: '', mode: 'CreditDeduction', createdByUserId: '', items: [defaultItem()] }, }) const { fields, append, remove } = useFieldArray({ control: form.control, name: 'items' }) const itemsById = useMemo(() => new Map(itemsList.map((i) => [i.id, i])), [itemsList]) - const eligibleOrders = useMemo( - () => ordersList.filter((o) => o.status !== 'Cancelled' && o.status !== 'Fulfilled'), - [ordersList] + const eligiblePOs = useMemo( + () => shopPOList.filter((o) => o.status !== 'Cancelled' && o.confirmedAt), + [shopPOList] ) useEffect(() => { if (!open) return - form.reset({ orderId: '', mode: 'CreditDeduction', createdByUserId: '', items: [defaultItem()] }) - setSelectedOrder(null) + form.reset({ shopPOId: '', mode: 'CreditDeduction', createdByUserId: '', items: [defaultItem()] }) + setSelectedPO(null) Promise.all([ - ordersApi.list(), - storesApi.list({ pageSize: 200 }), + shopPosApi.list({ pageSize: 200 }), itemsApi.list({ pageSize: 200 }), usersApi.list({ pageSize: 200 }), ]) - .then(([orderList, storeList, itemsRes, usersRes]) => { - setOrdersList(orderList) - setStoreNameById(new Map(getPaginatedItems(storeList).map((s) => [s.id, s.storeName]))) + .then(([posRes, itemsRes, usersRes]) => { + setShopPOList(getPaginatedItems(posRes)) setItemsList(getPaginatedItems(itemsRes)) setUsersList(getPaginatedItems(usersRes)) }) .catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })) }, [open]) - function handleOrderChange(orderId: string | null) { - form.setValue('orderId', orderId ?? '', { shouldValidate: true }) - const order = orderId ? ordersList.find((o) => o.id === orderId) ?? null : null - setSelectedOrder(order) + function handlePOChange(shopPOId: string | null) { + form.setValue('shopPOId', shopPOId ?? '', { shouldValidate: true }) + const po = shopPOId ? shopPOList.find((o) => o.id === shopPOId) ?? null : null + setSelectedPO(po) } async function onSubmit(values: FormValues) { try { - await saleReturnsApi.create(values.orderId, { + await saleReturnsApi.create(values.shopPOId, { mode: values.mode, items: values.items, createdByUserId: values.createdByUserId, }) - toast.add({ title: 'Order return recorded', type: 'success' }) + toast.add({ title: 'Return recorded', type: 'success' }) onSuccess() onClose() } catch (err) { @@ -123,14 +119,15 @@ export function OrderReturnDialog({ open, onClose, onSuccess }: OrderReturnDialo } } + const activeItems = selectedPO?.items.filter((i) => i.isActive) ?? [] + return ( { if (!v) onClose() }}> Record Order Return - Record items a store returned against an open order. Only items the store has previously - ordered can be returned. + Record items a store returned against a confirmed Shop PO. @@ -138,19 +135,19 @@ export function OrderReturnDialog({ open, onClose, onSuccess }: OrderReturnDialo ( - - + - Select order... - {eligibleOrders.map((o) => ( + Select Shop PO... + {eligiblePOs.map((o) => ( - {storeNameById.get(o.storeId) ?? o.storeId.slice(0, 8)} — {o.id.slice(0, 8).toUpperCase()} ({o.status}) + {o.storeName} — {o.id.slice(0, 8).toUpperCase()} ({o.status}) ))} @@ -200,7 +197,7 @@ export function OrderReturnDialog({ open, onClose, onSuccess }: OrderReturnDialo - {selectedOrder && ( + {selectedPO && (

Items to Return

@@ -223,11 +220,11 @@ export function OrderReturnDialog({ open, onClose, onSuccess }: OrderReturnDialo Select item... - {selectedOrder.items.map((oi) => { + {activeItems.map((oi) => { const item = itemsById.get(oi.itemId) return ( - {item?.name ?? oi.itemId.slice(0, 8)} {item ? `— ${item.sku}` : ''} + {oi.itemName}{item ? ` — ${item.sku}` : ''} ) })} @@ -282,7 +279,7 @@ export function OrderReturnDialog({ open, onClose, onSuccess }: OrderReturnDialo - diff --git a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-view-sheet.tsx b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-view-sheet.tsx index 4ba1660..8997cc3 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-view-sheet.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-view-sheet.tsx @@ -49,7 +49,7 @@ export function OrderReturnViewSheet({ open, onClose, ret, storeName }: OrderRet
{storeName && } - {ret.orderId}} /> + {ret.shopPOId}} /> {MODE_LABEL[ret.mode]}} /> diff --git a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-returns-client.tsx b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-returns-client.tsx index d11ff3d..5d28ec0 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-returns-client.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-returns-client.tsx @@ -9,7 +9,7 @@ import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/toast' -import { saleReturns as saleReturnsApi, orders as ordersApi, stores as storesApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import { saleReturns as saleReturnsApi, shopPos as shopPosApi, stores as storesApi, getApiError, getPaginatedItems } from '@/lib/api/index' import type { SaleReturnResponse, ReturnMode } from '@/types/api' import { OrderReturnViewSheet } from './order-return-view-sheet' import { OrderReturnDialog } from './order-return-dialog' @@ -49,13 +49,13 @@ export function OrderReturnsClient() { const fetchData = useCallback(async () => { setLoading(true) try { - const [returns, orderList, storeList] = await Promise.all([ + const [returns, shopPOList, storeList] = await Promise.all([ saleReturnsApi.list(), - ordersApi.list(), + shopPosApi.list({ pageSize: 200 }), storesApi.list({ pageSize: 200 }), ]) const storeNameById = new Map(getPaginatedItems(storeList).map((s) => [s.id, s.storeName])) - const map = new Map(orderList.map((o) => [o.id, storeNameById.get(o.storeId) ?? '—'])) + const map = new Map(getPaginatedItems(shopPOList).map((o) => [o.id, storeNameById.get(o.storeId) ?? o.storeName])) setStoreNameByOrderId(map) setData(returns) } catch (err) { @@ -78,10 +78,10 @@ export function OrderReturnsClient() { { key: 'store', title: 'Store', - fields: ['orderId'], + fields: ['shopPOId'], minWidth: 200, cellRender: (row) => ( - {storeNameByOrderId.get(row.orderId) ?? '—'} + {storeNameByOrderId.get(row.shopPOId) ?? '—'} ), }, { @@ -163,7 +163,7 @@ export function OrderReturnsClient() { open={viewOpen} onClose={() => setViewOpen(false)} ret={viewing} - storeName={viewing ? storeNameByOrderId.get(viewing.orderId) : undefined} + storeName={viewing ? storeNameByOrderId.get(viewing.shopPOId) : undefined} /> = { - Pending: 'secondary', - Completed: 'default', - Cancelled: 'destructive', -} - -const PAYMENT_STATUS_VARIANT: Record = { - Pending: 'secondary', - PartiallyPaid: 'secondary', - Paid: 'default', -} - -const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` -const truncId = (id: string) => id.substring(0, 8) - -const fmtDateTime = (v: string | null) => - v - ? new Date(v).toLocaleString('en-GB', { - day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', - }) - : '—' - -interface SaleViewSheetProps { - open: boolean - onClose: () => void - sale: SaleResponse | null -} - -function Row({ label, value }: { label: string; value: React.ReactNode }) { - return ( -
- {label} - {value} -
- ) -} - -export function SaleViewSheet({ open, onClose, sale }: SaleViewSheetProps) { - if (!sale) return null - - return ( - !v && onClose()}> - - - Sale Details - - -
- {truncId(sale.id)}} /> - {truncId(sale.storeId)}} /> - {sale.status}} /> - {sale.paymentStatus}} /> - - - - - - -
- - - -
-

Items ({sale.items.length})

-
- - - - - - - - - - - {sale.items.length === 0 ? ( - - - - ) : ( - sale.items.map((item) => ( - - - - - - - )) - )} - -
ItemQuantityFulfilledUnit Price
- No items on this sale. -
{truncId(item.itemId)}{item.quantity}{item.quantityFulfilled}{fmtAmount(item.unitPrice)}
-
-
-
-
- ) -} +// Replaced by shop-po-view-sheet.tsx +export {} diff --git a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx index 4f9c997..bb02cd2 100644 --- a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx @@ -1,7 +1,6 @@ 'use client' import { useState, useEffect, useCallback, useMemo } from 'react' -import { useRouter } from 'next/navigation' import { HugeiconsIcon } from '@hugeicons/react' import { Add01Icon } from '@hugeicons/core-free-icons' @@ -10,10 +9,11 @@ import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/toast' -import { sales as salesApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { SaleResponse, SaleStatus, PaymentStatus } from '@/types/api' +import { shopPos as shopPosApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { ShopPOResponse, ShopPOStatus } from '@/types/api' -import { SaleViewSheet } from './sale-view-sheet' +import { ShopPOViewSheet } from './shop-po-view-sheet' +import { ShopPOCreateSheet } from './shop-po-create-sheet' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -21,36 +21,31 @@ const fmtDate = (v: string) => const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` const truncId = (id: string) => id.substring(0, 8) -const SALE_STATUS_VARIANT: Record = { +const PO_STATUS_VARIANT: Record = { Pending: 'secondary', - Completed: 'default', + PartiallyFulfilled: 'secondary', + Fulfilled: 'default', Cancelled: 'destructive', } -const PAYMENT_STATUS_VARIANT: Record = { - Pending: 'secondary', - PartiallyPaid: 'secondary', - Paid: 'default', -} - export function SalesClient() { - const router = useRouter() - const [data, setData] = useState([]) + const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [viewOpen, setViewOpen] = useState(false) - const [selectedSale, setSelectedSale] = useState(null) + const [createOpen, setCreateOpen] = useState(false) + const [selectedPO, setSelectedPO] = useState(null) const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) try { - const res = await salesApi.list({ page: p, pageSize: ps }) + const res = await shopPosApi.list({ page: p, pageSize: ps }) setData(getPaginatedItems(res)) setTotal(res.total) } catch (err) { - toast.add({ title: 'Failed to load sales', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' }) } finally { setLoading(false) } @@ -60,45 +55,52 @@ export function SalesClient() { fetchData(page, pageSize) }, [fetchData, page, pageSize]) - function handleView(row: SaleResponse) { - setSelectedSale(row) + function handleView(row: ShopPOResponse) { + setSelectedPO(row) setViewOpen(true) } - async function handleCancel(row: SaleResponse) { + async function handleConfirm(row: ShopPOResponse) { try { - await salesApi.cancel(row.id) - toast.add({ title: 'Sale cancelled', type: 'success' }) + await shopPosApi.confirm(row.id) + toast.add({ title: 'Shop PO confirmed', type: 'success' }) fetchData(page, pageSize) } catch (err) { - toast.add({ title: 'Failed to cancel sale', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to confirm Shop PO', description: getApiError(err), type: 'error' }) } } - const tableConfig = useMemo>(() => ({ + async function handleCancel(row: ShopPOResponse) { + try { + await shopPosApi.cancel(row.id) + toast.add({ title: 'Shop PO cancelled', type: 'success' }) + fetchData(page, pageSize) + } catch (err) { + toast.add({ title: 'Failed to cancel Shop PO', description: getApiError(err), type: 'error' }) + } + } + + const tableConfig = useMemo>(() => ({ rowKey: 'id', serverSide: true, stickyHeader: true, - emptyMessage: 'No sales found.', + emptyMessage: 'No Shop POs found.', pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, columns: [ { key: 'id', - title: 'Sale ID', + title: 'PO ID', fields: ['id'], - width: 140, + width: 120, cellRender: (row) => ( {truncId(row.id)} ), }, { - key: 'storeId', - title: 'Store ID', - fields: ['storeId'], - width: 140, - cellRender: (row) => ( - {truncId(row.storeId)} - ), + key: 'store', + title: 'Store', + fields: [{ key: 'storeName', primary: true }], + minWidth: 180, }, { key: 'totalAmount', @@ -113,25 +115,21 @@ export function SalesClient() { key: 'status', title: 'Status', fields: ['status'], - width: 140, - cellRender: (row) => ( - {row.status} - ), - }, - { - key: 'paymentStatus', - title: 'Payment', - fields: ['paymentStatus'], width: 160, cellRender: (row) => ( - {row.paymentStatus} + {row.status} ), }, { - key: 'saleDate', - title: 'Sale Date', - fields: [{ key: 'saleDate', transform: (v) => fmtDate(v as string) }], - width: 140, + key: 'confirmed', + title: 'Confirmed', + fields: ['confirmedAt'], + width: 120, + cellRender: (row) => ( + + {row.confirmedAt ? fmtDate(row.confirmedAt) : 'Unconfirmed'} + + ), }, { key: 'createdAt', @@ -144,14 +142,20 @@ export function SalesClient() { actions: () => [ { action: 'view', - label: 'View sale', + label: 'View PO', onClick: handleView, }, + { + action: 'confirm', + label: 'Confirm PO', + onClick: handleConfirm, + visible: (target) => !target.confirmedAt && target.status !== 'Cancelled', + }, { action: 'cancel', - label: 'Cancel sale', + label: 'Cancel PO', onClick: handleCancel, - visible: (target) => target.status !== 'Cancelled', + visible: (target) => target.status !== 'Cancelled' && target.status !== 'Fulfilled', }, ], }, @@ -161,14 +165,14 @@ export function SalesClient() { <>
-

Sales

+

Shop POs

- Sales transactions recorded against stores + Purchase orders raised by agents on behalf of stores

-
@@ -183,10 +187,16 @@ export function SalesClient() { }} /> - setCreateOpen(false)} + onSuccess={() => fetchData(page, pageSize)} + /> + + setViewOpen(false)} - sale={selectedSale} + po={selectedPO} /> ) diff --git a/distribution_fe/app/(dashboard)/sales/_components/shop-po-create-sheet.tsx b/distribution_fe/app/(dashboard)/sales/_components/shop-po-create-sheet.tsx new file mode 100644 index 0000000..0fdad1d --- /dev/null +++ b/distribution_fe/app/(dashboard)/sales/_components/shop-po-create-sheet.tsx @@ -0,0 +1,217 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useForm, zodResolver, z } from '@/lib/form' +import { FormField } from '@/components/shared/form-field' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { FieldGroup } from '@/components/ui/field' +import { toast } from '@/components/ui/toast' +import { shopPos as shopPosApi, stores as storesApi, items as itemsApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { StoreResponse, ItemSummaryResponse } from '@/types/api' + +const itemLineSchema = z.object({ + itemId: z.string().min(1, 'Item required'), + quantity: z.number().int().min(1, 'Min 1'), + unitPrice: z.number().min(0, 'Price must be 0 or more'), +}) + +const schema = z.object({ + storeId: z.string().min(1, 'Store is required'), + items: z.array(itemLineSchema).min(1, 'At least one item is required'), +}) + +type FormValues = z.infer + +interface ShopPOCreateSheetProps { + open: boolean + onClose: () => void + onSuccess: () => void +} + +const emptyLine = { itemId: '', quantity: 1, unitPrice: 0 } + +export function ShopPOCreateSheet({ open, onClose, onSuccess }: ShopPOCreateSheetProps) { + const [storesList, setStoresList] = useState([]) + const [itemsList, setItemsList] = useState([]) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { storeId: '', items: [emptyLine] }, + }) + + const watchedItems = form.watch('items') + + useEffect(() => { + if (!open) return + form.reset({ storeId: '', items: [emptyLine] }) + Promise.all([ + storesApi.list({ pageSize: 200 }), + itemsApi.list({ pageSize: 200 }), + ]) + .then(([s, i]) => { + setStoresList(getPaginatedItems(s)) + setItemsList(getPaginatedItems(i)) + }) + .catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })) + }, [open]) + + function addLine() { + form.setValue('items', [...watchedItems, emptyLine]) + } + + function removeLine(idx: number) { + form.setValue('items', watchedItems.filter((_, i) => i !== idx)) + } + + async function onSubmit(values: FormValues) { + try { + await shopPosApi.create(values) + toast.add({ title: 'Shop PO created', type: 'success' }) + onSuccess() + onClose() + } catch (err) { + toast.add({ title: 'Failed to create Shop PO', description: getApiError(err), type: 'error' }) + } + } + + return ( + !v && onClose()}> + + + New Shop PO + Raise a purchase order on behalf of a store. + + + + + ( + + )} + /> + + +
+
+

Line Items

+ +
+ +
+ {watchedItems.map((_, idx) => ( +
+ ( + + )} + /> + ( + field.onChange(Number(e.target.value))} + onBlur={field.onBlur} + aria-invalid={invalid} + /> + )} + /> + ( + field.onChange(Number(e.target.value))} + onBlur={field.onBlur} + aria-invalid={invalid} + /> + )} + /> +
+ +
+
+ ))} +
+
+ + + + + + +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/sales/_components/shop-po-view-sheet.tsx b/distribution_fe/app/(dashboard)/sales/_components/shop-po-view-sheet.tsx new file mode 100644 index 0000000..911233c --- /dev/null +++ b/distribution_fe/app/(dashboard)/sales/_components/shop-po-view-sheet.tsx @@ -0,0 +1,106 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Separator } from '@/components/ui/separator' +import type { ShopPOResponse, ShopPOStatus } from '@/types/api' + +const PO_STATUS_VARIANT: Record = { + Pending: 'secondary', + PartiallyFulfilled: 'secondary', + Fulfilled: 'default', + Cancelled: 'destructive', +} + +const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` +const truncId = (id: string) => id.substring(0, 8) + +const fmtDateTime = (v: string | null) => + v + ? new Date(v).toLocaleString('en-GB', { + day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', + }) + : '—' + +interface ShopPOViewSheetProps { + open: boolean + onClose: () => void + po: ShopPOResponse | null +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ) +} + +export function ShopPOViewSheet({ open, onClose, po }: ShopPOViewSheetProps) { + if (!po) return null + + return ( + !v && onClose()}> + + + Shop PO Details + + +
+ {truncId(po.id)}} /> + + {po.status}} /> + + {truncId(po.invoiceId)} : 'No invoice yet'} /> + + + +
+ + + +
+

Line Items ({po.items.filter(i => i.isActive).length})

+
+ + + + + + + + + + + + {po.items.filter(i => i.isActive).length === 0 ? ( + + + + ) : ( + po.items.filter(i => i.isActive).map((item) => ( + + + + + + + + )) + )} + +
ItemQtyFulfilledUnit PriceLine Total
+ No items on this PO. +
{item.itemName}{item.quantity}{item.quantityFulfilled}{fmtAmount(item.unitPrice)}{fmtAmount(item.quantity * item.unitPrice)}
+
+
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/sales/new/page.tsx b/distribution_fe/app/(dashboard)/sales/new/page.tsx index 6d04845..d384969 100644 --- a/distribution_fe/app/(dashboard)/sales/new/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/new/page.tsx @@ -15,24 +15,21 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { Checkbox } from '@/components/ui/checkbox' import { Button } from '@/components/ui/button' import { FieldGroup } from '@/components/ui/field' import { Separator } from '@/components/ui/separator' import { toast } from '@/components/ui/toast' import { - sales as salesApi, + shopPos as shopPosApi, stores as storesApi, - rides as ridesApi, items as itemsApi, - users as usersApi, priceLists as priceListsApi, getApiError, getPaginatedItems, } from '@/lib/api/index' -import type { ItemSummaryResponse, RideResponse, StoreResponse, UserResponse } from '@/types/api' +import type { ItemSummaryResponse, StoreResponse } from '@/types/api' -const saleItemSchema = z.object({ +const lineItemSchema = z.object({ itemId: z.string().min(1, 'Item is required'), quantity: z.number().min(1, 'Quantity must be at least 1'), unitPrice: z.number().min(0.01, 'Unit price must be greater than 0'), @@ -40,11 +37,7 @@ const saleItemSchema = z.object({ const schema = z.object({ storeId: z.string().min(1, 'Store is required'), - saleDate: z.string().min(1, 'Sale date is required'), - createdByUserId: z.string().min(1, 'Created by is required'), - rideId: z.string(), - fulfillImmediately: z.boolean(), - items: z.array(saleItemSchema).min(1, 'At least one item is required'), + items: z.array(lineItemSchema).min(1, 'At least one item is required'), }) type FormValues = z.infer @@ -53,61 +46,46 @@ function createDefaultItem() { return { itemId: '', quantity: 1, unitPrice: 0 } } -function todayIso() { - return new Date().toISOString().slice(0, 10) -} - const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` -export default function NewSalePage() { +export default function NewShopPOPage() { const router = useRouter() const [storesList, setStoresList] = useState([]) - const [ridesList, setRidesList] = useState([]) const [itemsList, setItemsList] = useState([]) - const [usersList, setUsersList] = useState([]) + const [priceMinByIndex, setPriceMinByIndex] = useState>({}) + const [priceRetailByIndex, setPriceRetailByIndex] = useState>({}) const form = useForm({ resolver: zodResolver(schema), - defaultValues: { - storeId: '', - saleDate: todayIso(), - createdByUserId: '', - rideId: '', - fulfillImmediately: false, - items: [createDefaultItem()], - }, - }) - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: 'items', + defaultValues: { storeId: '', items: [createDefaultItem()] }, }) + const { fields, append, remove } = useFieldArray({ control: form.control, name: 'items' }) const watchedItems = useWatch({ control: form.control, name: 'items' }) const watchedStoreId = useWatch({ control: form.control, name: 'storeId' }) - const watchedSaleDate = useWatch({ control: form.control, name: 'saleDate' }) + const itemsById = useMemo( () => new Map(itemsList.map((item) => [item.id, item])), [itemsList] ) + const totalAmount = useMemo( - () => watchedItems.reduce((sum, item) => sum + (Number(item.quantity) || 0) * (Number(item.unitPrice) || 0), 0), + () => watchedItems.reduce((sum, line) => sum + (Number(line.quantity) || 0) * (Number(line.unitPrice) || 0), 0), [watchedItems] ) - const [priceMinByIndex, setPriceMinByIndex] = useState>({}) - const [priceRetailByIndex, setPriceRetailByIndex] = useState>({}) async function resolveAndSetPrice(index: number, itemId: string) { const item = itemsById.get(itemId) - if (watchedStoreId && watchedSaleDate) { + const today = new Date().toISOString().slice(0, 10) + if (watchedStoreId) { try { - const resolution = await priceListsApi.resolve({ itemId, storeId: watchedStoreId, saleDate: watchedSaleDate }) + const resolution = await priceListsApi.resolve({ itemId, storeId: watchedStoreId, saleDate: today }) form.setValue(`items.${index}.unitPrice`, resolution.price, { shouldValidate: true }) setPriceMinByIndex((prev) => ({ ...prev, [index]: resolution.minPrice })) setPriceRetailByIndex((prev) => ({ ...prev, [index]: resolution.retailPrice })) return } catch { - // No resolvable price for this store/date (e.g. store/item not found) — fall back below. + // No resolvable price for this store — fall back to item default. } } if (item) { @@ -117,28 +95,22 @@ export default function NewSalePage() { } } - // Re-resolve every already-selected line's price whenever the store or sale date changes, - // since the applicable price list is scoped by the store's region and the sale date. useEffect(() => { - if (!watchedStoreId || !watchedSaleDate) return + if (!watchedStoreId) return watchedItems.forEach((line, index) => { if (line.itemId) resolveAndSetPrice(index, line.itemId) }) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [watchedStoreId, watchedSaleDate]) + }, [watchedStoreId]) useEffect(() => { Promise.all([ storesApi.list({ pageSize: 200 }), - ridesApi.list({ pageSize: 200, rideType: 'Sale' }), itemsApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), ]) - .then(([stores, rides, items, usersRes]) => { + .then(([stores, items]) => { setStoresList(getPaginatedItems(stores)) - setRidesList(getPaginatedItems(rides)) setItemsList(getPaginatedItems(items)) - setUsersList(getPaginatedItems(usersRes)) }) .catch((err) => { toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) @@ -147,18 +119,14 @@ export default function NewSalePage() { async function onSubmit(values: FormValues) { try { - await salesApi.create({ + await shopPosApi.create({ storeId: values.storeId, - saleDate: values.saleDate, - rideId: values.rideId || null, - fulfillImmediately: values.fulfillImmediately, items: values.items, - createdByUserId: values.createdByUserId, }) - toast.add({ title: 'Sale created', type: 'success' }) + toast.add({ title: 'Shop PO created', type: 'success' }) router.push('/sales') } catch (err) { - toast.add({ title: 'Failed to create sale', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to create Shop PO', description: getApiError(err), type: 'error' }) } } @@ -170,116 +138,34 @@ export default function NewSalePage() { Back
-

Create Sale

-

Record a sale for a store with one or more items.

+

Create Shop PO

+

Record a purchase order for a store.

-

Sale Details

+

Order Details

-
- ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> -
- ( - + )} />
@@ -313,7 +199,9 @@ export default function NewSalePage() { {fields.map((fieldItem, index) => { const selectedItem = itemsById.get(watchedItems[index]?.itemId ?? '') - const lineTotal = (Number(watchedItems[index]?.quantity) || 0) * (Number(watchedItems[index]?.unitPrice) || 0) + const lineTotal = + (Number(watchedItems[index]?.quantity) || 0) * + (Number(watchedItems[index]?.unitPrice) || 0) return ( @@ -336,7 +224,7 @@ export default function NewSalePage() { Select item... {itemsList.map((item) => ( - {item.name} - {item.sku} + {item.name} — {item.sku} ))} @@ -356,9 +244,6 @@ export default function NewSalePage() { name={`items.${index}.quantity`} render={({ field, invalid }) => ( ( {priceMinByIndex[index] != null && (

- Negotiable down to {fmtAmount(priceMinByIndex[index]!)} + Min: {fmtAmount(priceMinByIndex[index]!)}

)} {priceRetailByIndex[index] != null && (

- Retail price: {fmtAmount(priceRetailByIndex[index]!)} + Retail: {fmtAmount(priceRetailByIndex[index]!)}

)} @@ -426,9 +308,9 @@ export default function NewSalePage() {
-

Sale Summary

+

Order Summary

- {watchedItems.length} item{watchedItems.length === 1 ? '' : 's'} on this sale. + {watchedItems.length} item{watchedItems.length === 1 ? '' : 's'} on this order.

@@ -440,7 +322,7 @@ export default function NewSalePage() {
- - - - -
- ) -} +// Retained for compatibility — invoice status transitions are now handled inline via table row actions. +export {} diff --git a/distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx b/distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx index ab2a68d..3a82da6 100644 --- a/distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx +++ b/distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx @@ -7,14 +7,18 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' -import { Separator } from '@/components/ui/separator' -import type { OrderResponse, OrderStatus } from '@/types/api' +import type { InvoiceResponse, InvoiceStatus, PaymentStatus } from '@/types/api' -const ORDER_STATUS_VARIANT: Record = { +const INVOICE_STATUS_VARIANT: Record = { + Draft: 'secondary', + Printed: 'secondary', + Finalized: 'default', +} + +const PAYMENT_STATUS_VARIANT: Record = { Pending: 'secondary', - PartiallyFulfilled: 'secondary', - Fulfilled: 'default', - Cancelled: 'destructive', + PartiallyPaid: 'secondary', + Paid: 'default', } const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` @@ -30,8 +34,7 @@ const fmtDateTime = (v: string | null) => interface OrderViewSheetProps { open: boolean onClose: () => void - order: OrderResponse | null - storeName?: string + invoice: InvoiceResponse | null } function Row({ label, value }: { label: string; value: React.ReactNode }) { @@ -43,61 +46,26 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) { ) } -export function OrderViewSheet({ open, onClose, order, storeName }: OrderViewSheetProps) { - if (!order) return null +export function OrderViewSheet({ open, onClose, invoice }: OrderViewSheetProps) { + if (!invoice) return null return ( !v && onClose()}> - + - Order Details + Invoice Details
- {truncId(order.id)}} /> - {truncId(order.saleId)}} /> - {truncId(order.storeId)}} /> - {order.status}} /> - - - - -
- - - -
-

Items ({order.items.length})

-
- - - - - - - - - - - {order.items.length === 0 ? ( - - - - ) : ( - order.items.map((item) => ( - - - - - - - )) - )} - -
ItemQuantityFulfilledUnit Price
- No items on this order. -
{truncId(item.itemId)}{item.quantity}{item.quantityFulfilled}{fmtAmount(item.unitPrice)}
-
+ {truncId(invoice.id)}} /> + {truncId(invoice.shopPOId)}} /> + + {invoice.status}} /> + {invoice.paymentStatus}} /> + + + +
diff --git a/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx b/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx index 6499008..980a219 100644 --- a/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx @@ -6,11 +6,10 @@ import { DataTable } from '@/components/shared/data-table' import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { toast } from '@/components/ui/toast' -import { orders as ordersApi, stores as storesApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { OrderResponse, OrderStatus, StoreResponse } from '@/types/api' +import { invoices as invoicesApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { InvoiceResponse, InvoiceStatus, PaymentStatus } from '@/types/api' import { OrderViewSheet } from './order-view-sheet' -import { OrderEditStatusDialog } from './order-edit-status-dialog' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -18,78 +17,80 @@ const fmtDate = (v: string) => const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` const truncId = (id: string) => id.substring(0, 8) -const ORDER_STATUS_VARIANT: Record = { +const INVOICE_STATUS_VARIANT: Record = { + Draft: 'secondary', + Printed: 'secondary', + Finalized: 'default', +} + +const PAYMENT_STATUS_VARIANT: Record = { Pending: 'secondary', - PartiallyFulfilled: 'secondary', - Fulfilled: 'default', - Cancelled: 'destructive', + PartiallyPaid: 'secondary', + Paid: 'default', } export function OrdersClient() { - const [data, setData] = useState([]) + const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [viewOpen, setViewOpen] = useState(false) - const [editOpen, setEditOpen] = useState(false) - const [selectedOrder, setSelectedOrder] = useState(null) - const [storesList, setStoresList] = useState([]) + const [selectedInvoice, setSelectedInvoice] = useState(null) - const storesById = useMemo( - () => new Map(storesList.map((s) => [s.id, s.storeName])), - [storesList] - ) - - const fetchData = useCallback(async () => { + const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) try { - const items = await ordersApi.list() - setData(items) - setTotal(items.length) + const res = await invoicesApi.list({ page: p, pageSize: ps }) + setData(getPaginatedItems(res)) + setTotal(res.total) } catch (err) { - toast.add({ title: 'Failed to load orders', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to load invoices', description: getApiError(err), type: 'error' }) } finally { setLoading(false) } }, []) useEffect(() => { - fetchData() - storesApi.list({ pageSize: 200 }) - .then((res) => setStoresList(getPaginatedItems(res))) - .catch((err) => toast.add({ title: 'Failed to load stores', description: getApiError(err), type: 'error' })) - }, [fetchData]) + fetchData(page, pageSize) + }, [fetchData, page, pageSize]) - function handleView(row: OrderResponse) { - setSelectedOrder(row) + function handleView(row: InvoiceResponse) { + setSelectedInvoice(row) setViewOpen(true) } - function handleEdit(row: OrderResponse) { - setSelectedOrder(row) - setEditOpen(true) - } - - async function handleDelete(row: OrderResponse) { + async function handlePrint(row: InvoiceResponse) { try { - await ordersApi.delete(row.id) - toast.add({ title: 'Order cancelled', type: 'success' }) - fetchData() + await invoicesApi.print(row.id) + toast.add({ title: 'Invoice marked as printed', type: 'success' }) + fetchData(page, pageSize) } catch (err) { - toast.add({ title: 'Failed to cancel order', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to print invoice', description: getApiError(err), type: 'error' }) } } - const tableConfig = useMemo>(() => ({ + async function handleFinalize(row: InvoiceResponse) { + try { + await invoicesApi.finalize(row.id) + toast.add({ title: 'Invoice finalized', type: 'success' }) + fetchData(page, pageSize) + } catch (err) { + toast.add({ title: 'Failed to finalize invoice', description: getApiError(err), type: 'error' }) + } + } + + const tableConfig = useMemo>(() => ({ rowKey: 'id', + serverSide: true, stickyHeader: true, - emptyMessage: 'No orders found.', + emptyMessage: 'No invoices found.', pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, columns: [ { key: 'id', - title: 'Order ID', + title: 'Invoice ID', fields: ['id'], width: 140, cellRender: (row) => ( @@ -97,22 +98,10 @@ export function OrdersClient() { ), }, { - key: 'saleId', - title: 'Sale ID', - fields: ['saleId'], - width: 140, - cellRender: (row) => ( - {truncId(row.saleId)} - ), - }, - { - key: 'storeId', + key: 'store', title: 'Store', - fields: ['storeId'], - minWidth: 160, - cellRender: (row) => ( - {storesById.get(row.storeId) ?? truncId(row.storeId)} - ), + fields: [{ key: 'storeName', primary: true }], + minWidth: 180, }, { key: 'totalAmount', @@ -127,16 +116,19 @@ export function OrdersClient() { key: 'status', title: 'Status', fields: ['status'], - width: 180, + width: 140, cellRender: (row) => ( - {row.status} + {row.status} ), }, { - key: 'orderDate', - title: 'Order Date', - fields: [{ key: 'orderDate', transform: (v) => fmtDate(v as string) }], - width: 140, + key: 'paymentStatus', + title: 'Payment', + fields: ['paymentStatus'], + width: 150, + cellRender: (row) => ( + {row.paymentStatus} + ), }, { key: 'createdAt', @@ -149,32 +141,32 @@ export function OrdersClient() { actions: () => [ { action: 'view', - label: 'View order', + label: 'View invoice', onClick: handleView, }, { - action: 'edit', - label: 'Edit status', - onClick: handleEdit, - visible: (target) => target.status !== 'Cancelled', + action: 'print', + label: 'Mark as printed', + onClick: handlePrint, + visible: (target) => target.status === 'Draft', }, { - action: 'cancel', - label: 'Delete order', - onClick: handleDelete, - visible: (target) => target.status !== 'Cancelled', + action: 'finalize', + label: 'Finalize invoice', + onClick: handleFinalize, + visible: (target) => target.status === 'Printed', }, ], }, - }), [pageSize, storesById]) + }), [pageSize]) return ( <>
-

Orders

+

Invoices

- Fulfillment status of store orders + Billing documents generated from confirmed Shop POs

@@ -185,6 +177,7 @@ export function OrdersClient() { loading={loading} totalCount={total} onServerSideChange={({ page: p }) => { + setPage(p.page) setPageSize(p.pageSize) }} /> @@ -192,15 +185,7 @@ export function OrdersClient() { setViewOpen(false)} - order={selectedOrder} - storeName={selectedOrder ? storesById.get(selectedOrder.storeId) : undefined} - /> - - setEditOpen(false)} - onSuccess={fetchData} - order={selectedOrder} + invoice={selectedInvoice} /> ) diff --git a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx index 28e945f..1d47c16 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { useForm, zodResolver, z } from '@/lib/form' import { FormField } from '@/components/shared/form-field' import { Input } from '@/components/ui/input' @@ -22,14 +22,13 @@ import { } from '@/components/ui/dialog' import { FieldGroup } from '@/components/ui/field' import { toast } from '@/components/ui/toast' -import { payments as paymentsApi, sales as salesApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { SaleResponse, UserResponse } from '@/types/api' +import { payments as paymentsApi, getApiError } from '@/lib/api/index' +import type { InvoiceResponse } from '@/types/api' const schema = z.object({ - saleId: z.string().min(1, 'Sale is required'), + invoiceId: z.string().min(1, 'Invoice is required'), amount: z.number().min(0.01, 'Amount must be greater than 0'), method: z.enum(['Cash', 'Credit']), - collectedByUserId: z.string().min(1, 'Collected by is required'), }) type FormValues = z.infer @@ -38,51 +37,31 @@ interface PaymentSheetProps { open: boolean onClose: () => void onSuccess: () => void + invoices: InvoiceResponse[] } const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` const truncId = (id: string) => id.substring(0, 8) -export function PaymentSheet({ open, onClose, onSuccess }: PaymentSheetProps) { - const [salesList, setSalesList] = useState([]) - const [usersList, setUsersList] = useState([]) - +export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentSheetProps) { const form = useForm({ resolver: zodResolver(schema), defaultValues: { - saleId: '', + invoiceId: '', amount: 0, method: 'Cash', - collectedByUserId: '', }, }) useEffect(() => { if (!open) return - - form.reset({ - saleId: '', - amount: 0, - method: 'Cash', - collectedByUserId: '', - }) - - Promise.all([ - salesApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), - ]) - .then(([salesRes, usersRes]) => { - setSalesList(getPaginatedItems(salesRes).filter((s) => s.status !== 'Cancelled')) - setUsersList(getPaginatedItems(usersRes)) - }) - .catch((err) => { - toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) - }) + form.reset({ invoiceId: '', amount: 0, method: 'Cash' }) }, [open]) async function onSubmit(values: FormValues) { + const { invoiceId, ...rest } = values try { - await paymentsApi.create(values) + await paymentsApi.create(invoiceId, rest) toast.add({ title: 'Payment recorded', type: 'success' }) onSuccess() onClose() @@ -91,31 +70,33 @@ export function PaymentSheet({ open, onClose, onSuccess }: PaymentSheetProps) { } } + const pendingInvoices = invoices.filter((inv) => inv.paymentStatus !== 'Paid') + return ( !v && onClose()}> Record Payment - Record a cash or credit payment against a sale. + Record a cash or credit payment against an invoice.
( + )} /> - - ( - - )} - /> diff --git a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx index 1468570..2aa4a01 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx @@ -26,8 +26,7 @@ interface PaymentViewSheetProps { open: boolean onClose: () => void payment: PaymentResponse | null - storeName?: string - collectedByName?: string + invoiceName?: string } function Row({ label, value }: { label: string; value: React.ReactNode }) { @@ -39,7 +38,7 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) { ) } -export function PaymentViewSheet({ open, onClose, payment, storeName, collectedByName }: PaymentViewSheetProps) { +export function PaymentViewSheet({ open, onClose, payment, invoiceName }: PaymentViewSheetProps) { if (!payment) return null return ( @@ -51,12 +50,11 @@ export function PaymentViewSheet({ open, onClose, payment, storeName, collectedB
{truncId(payment.id)}} /> - {truncId(payment.saleId)}} /> - {truncId(payment.storeId)}} /> + {truncId(payment.invoiceId)}} /> {payment.method}} /> - {truncId(payment.collectedBy)}} /> + {truncId(payment.collectedBy)}} />
diff --git a/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx b/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx index 8327b16..cf1ed72 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx @@ -8,9 +8,10 @@ import { DataTable } from '@/components/shared/data-table' import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' import { toast } from '@/components/ui/toast' -import { payments as paymentsApi, stores as storesApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { PaymentResponse, PaymentMethod, StoreResponse, UserResponse } from '@/types/api' +import { invoices as invoicesApi, payments as paymentsApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { PaymentResponse, PaymentMethod, InvoiceResponse } from '@/types/api' import { PaymentSheet } from './payment-sheet' import { PaymentViewSheet } from './payment-view-sheet' @@ -27,33 +28,39 @@ const PAYMENT_METHOD_VARIANT: Record = { } export function PaymentsClient() { + const [invoicesList, setInvoicesList] = useState([]) + const [selectedInvoiceId, setSelectedInvoiceId] = useState('') const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) + const [loading, setLoading] = useState(false) const [total, setTotal] = useState(0) - const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(20) + const [pageSize] = useState(20) const [sheetOpen, setSheetOpen] = useState(false) const [viewOpen, setViewOpen] = useState(false) const [selectedPayment, setSelectedPayment] = useState(null) - const [storesList, setStoresList] = useState([]) - const [usersList, setUsersList] = useState([]) - const storeNameById = useMemo( - () => new Map(storesList.map((s) => [s.id, s.storeName])), - [storesList] - ) - const userNameById = useMemo( - () => new Map(usersList.map((u) => [u.id, u.fullName])), - [usersList] + const invoiceNameById = useMemo( + () => new Map(invoicesList.map((inv) => [inv.id, `${inv.storeName} — ${truncId(inv.id)}`])), + [invoicesList] ) - const fetchData = useCallback(async (p: number, ps: number) => { + useEffect(() => { + invoicesApi.list({ pageSize: 200 }) + .then((res) => setInvoicesList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load invoices', description: getApiError(err), type: 'error' })) + }, []) + + const fetchData = useCallback(async (invoiceId: string) => { + if (!invoiceId) { + setData([]) + setTotal(0) + return + } setLoading(true) try { - const res = await paymentsApi.list({ page: p, pageSize: ps }) - setData(getPaginatedItems(res)) - setTotal(res.total) + const items = await paymentsApi.listByInvoice(invoiceId) + setData(items) + setTotal(items.length) } catch (err) { toast.add({ title: 'Failed to load payments', description: getApiError(err), type: 'error' }) } finally { @@ -62,17 +69,8 @@ export function PaymentsClient() { }, []) useEffect(() => { - fetchData(page, pageSize) - }, [fetchData, page, pageSize]) - - useEffect(() => { - storesApi.list({ pageSize: 200 }) - .then((res) => setStoresList(getPaginatedItems(res))) - .catch((err) => toast.add({ title: 'Failed to load stores', description: getApiError(err), type: 'error' })) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) - .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) - }, []) + fetchData(selectedInvoiceId) + }, [fetchData, selectedInvoiceId]) function handleView(row: PaymentResponse) { setSelectedPayment(row) @@ -81,9 +79,8 @@ export function PaymentsClient() { const tableConfig = useMemo>(() => ({ rowKey: 'id', - serverSide: true, stickyHeader: true, - emptyMessage: 'No payments found.', + emptyMessage: selectedInvoiceId ? 'No payments found for this invoice.' : 'Enter an invoice ID to view its payments.', pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, columns: [ { @@ -96,12 +93,12 @@ export function PaymentsClient() { ), }, { - key: 'storeId', - title: 'Store', - fields: ['storeId'], - minWidth: 160, + key: 'invoiceId', + title: 'Invoice', + fields: ['invoiceId'], + minWidth: 180, cellRender: (row) => ( - {storeNameById.get(row.storeId) ?? truncId(row.storeId)} + {invoiceNameById.get(row.invoiceId) ?? truncId(row.invoiceId)} ), }, { @@ -128,15 +125,6 @@ export function PaymentsClient() { fields: [{ key: 'collectedAt', transform: (v) => fmtDate(v as string) }], width: 140, }, - { - key: 'collectedBy', - title: 'Collected By', - fields: ['collectedBy'], - minWidth: 160, - cellRender: (row) => ( - {userNameById.get(row.collectedBy) ?? truncId(row.collectedBy)} - ), - }, ], rowActions: { actions: () => [ @@ -147,7 +135,7 @@ export function PaymentsClient() { }, ], }, - }), [pageSize, storeNameById, userNameById]) + }), [pageSize, invoiceNameById]) return ( <> @@ -155,7 +143,7 @@ export function PaymentsClient() {

Payments

- Cash and credit payments collected from stores + Cash and credit payments collected against invoices

+
+ setSelectedInvoiceId(e.target.value.trim())} + /> + {selectedInvoiceId && ( + + )} +
+ { - setPage(p.page) - setPageSize(p.pageSize) - }} /> setSheetOpen(false)} - onSuccess={() => fetchData(page, pageSize)} + onSuccess={() => fetchData(selectedInvoiceId)} + invoices={invoicesList} /> setViewOpen(false)} payment={selectedPayment} - storeName={selectedPayment ? storeNameById.get(selectedPayment.storeId) : undefined} - collectedByName={selectedPayment ? userNameById.get(selectedPayment.collectedBy) : undefined} + invoiceName={selectedPayment ? invoiceNameById.get(selectedPayment.invoiceId) : undefined} /> ) diff --git a/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx b/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx index 467aeaf..420b278 100644 --- a/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx +++ b/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx @@ -50,7 +50,7 @@ type FormValues = z.infer interface StoreSheetProps { open: boolean onClose: () => void - store: StoreResponse + store: StoreResponse | null onSuccess: () => void } @@ -81,7 +81,7 @@ export function StoreSheet({ open, onClose, store, onSuccess }: StoreSheetProps) toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' }) ) - form.reset({ + if (store) form.reset({ storeName: store.storeName, ownerName: store.ownerName, storeAddress: store.storeAddress, @@ -97,7 +97,7 @@ export function StoreSheet({ open, onClose, store, onSuccess }: StoreSheetProps) async function onSubmit(values: FormValues) { try { - await storesApi.update(store.id, { + await storesApi.update(store!.id, { ...values, segment: values.segment || undefined, }) diff --git a/distribution_fe/components/shared/data-table/types.ts b/distribution_fe/components/shared/data-table/types.ts index 47cac6c..e7e6708 100644 --- a/distribution_fe/components/shared/data-table/types.ts +++ b/distribution_fe/components/shared/data-table/types.ts @@ -10,7 +10,9 @@ export type RowActionKind = | 'deactivate' | 'confirm' | 'close' - | 'cancel'; + | 'cancel' + | 'print' + | 'finalize'; export interface BadgeConfig { colorMap?: Record; diff --git a/distribution_fe/components/shared/row-actions.tsx b/distribution_fe/components/shared/row-actions.tsx index 01037d9..05b6487 100644 --- a/distribution_fe/components/shared/row-actions.tsx +++ b/distribution_fe/components/shared/row-actions.tsx @@ -5,9 +5,11 @@ import { HugeiconsIcon } from "@hugeicons/react" import { Cancel01Icon, CheckmarkCircle02Icon, + CheckmarkSquare01Icon, Delete02Icon, EyeIcon, PencilEdit01Icon, + PrinterIcon, } from "@hugeicons/core-free-icons" import { Button } from "@/components/ui/button" @@ -20,6 +22,8 @@ const ACTION_ICONS = { confirm: CheckmarkCircle02Icon, deactivate: Cancel01Icon, edit: PencilEdit01Icon, + finalize: CheckmarkSquare01Icon, + print: PrinterIcon, view: EyeIcon, } as const diff --git a/distribution_fe/config/nav.ts b/distribution_fe/config/nav.ts index 315219c..fa863d2 100644 --- a/distribution_fe/config/nav.ts +++ b/distribution_fe/config/nav.ts @@ -60,12 +60,12 @@ export const NAV_GROUPS: NavGroup[] = [ ], }, { - title: 'Sales & Orders', + title: 'Shop POs & Invoices', icon: DollarSquareIcon, items: [ - { title: 'Sales', href: '/sales', icon: DollarSquareIcon, description: 'Sales transactions recorded against stores', permission: 'orders.view' }, - { title: 'Orders', href: '/sales/orders', icon: ClipboardListIcon, description: 'Fulfillment status of store orders', permission: 'orders.view' }, - { title: 'Payments', href: '/sales/payments', icon: BankIcon, description: 'Cash and credit payments collected from stores', permission: 'orders.view' }, + { title: 'Shop POs', href: '/sales', icon: ClipboardListIcon, description: 'Purchase orders raised by sales agents on behalf of stores', permission: 'orders.view' }, + { title: 'Invoices', href: '/sales/orders', icon: DollarSquareIcon, description: 'Billing documents generated from confirmed Shop POs', permission: 'orders.view' }, + { title: 'Payments', href: '/sales/payments', icon: BankIcon, description: 'Cash and credit payments collected against invoices', permission: 'orders.view' }, ], }, { @@ -161,6 +161,6 @@ export const BOTTOM_NAV_CANDIDATES: NavItem[] = [ { title: 'Stores', href: '/stores', icon: Store01Icon, permission: 'stores.view' }, { title: 'Rides', href: '/operations/rides', icon: TruckIcon, permission: 'rides.view' }, { title: 'Loading', href: '/operations/loading', icon: DeliveryBox01Icon, permission: 'loading.view' }, - { title: 'Sales', href: '/sales', icon: DollarSquareIcon, permission: 'orders.view' }, + { title: 'Shop POs', href: '/sales', icon: ClipboardListIcon, permission: 'orders.view' }, { title: 'Items', href: '/catalogue/items', icon: Package01Icon, permission: 'items.view' }, ] diff --git a/distribution_fe/lib/api/index.ts b/distribution_fe/lib/api/index.ts index 8b6a06b..65c81eb 100644 --- a/distribution_fe/lib/api/index.ts +++ b/distribution_fe/lib/api/index.ts @@ -8,7 +8,7 @@ export { drivers, compliance } from './drivers' export { purchaseOrders, grn, purchaseReturns } from './procurement' export { rides, loadingSheets, deliveryTasks } from './operations' export { stock } from './stock' -export { sales, orders, payments } from './sales' +export { shopPos, invoices, payments } from './sales' export { saleReturns, claims, supplierClaims } from './returns-claims' export { getPaginatedItems } from './paginated' diff --git a/distribution_fe/lib/api/operations.ts b/distribution_fe/lib/api/operations.ts index 00d6a62..7abd22f 100644 --- a/distribution_fe/lib/api/operations.ts +++ b/distribution_fe/lib/api/operations.ts @@ -2,10 +2,8 @@ import { api } from '@/lib/api' import type { PaginatedResponse, RideResponse, - CancelRequestResponse, LoadingSheetResponse, DeliveryTaskResponse, - RideType, RideStatus, CancelRequestType, LoadingSheetStatus, @@ -16,22 +14,18 @@ import type { PageParams } from './master' // ─── Rides ──────────────────────────────────────────────────────────────────── export interface RideBody { - rideType: RideType vehicleId: string driverProfileId: string - routeId: string - sourceWarehouseId: string meterstart: number } export interface RideListParams extends PageParams { status?: RideStatus - rideType?: RideType driverProfileId?: string vehicleId?: string } -export interface CancelRequestBody { +export interface CancelRideBody { typeCode: CancelRequestType reason: string } @@ -49,43 +43,24 @@ export const rides = { assignDriver: (id: string, driverProfileId: string) => api.put(`/rides/${id}/driver`, { driverProfileId }).then(r => r.data), - activate: (id: string) => - api.patch(`/rides/${id}/activate`).then(r => r.data), - close: (id: string, meterend: number) => - api.patch(`/rides/${id}/close`, { meterend }).then(r => r.data), + api.post(`/rides/${id}/close`, { meterend }).then(r => r.data), - cancelRequests: { - getForRide: (rideId: string) => - api.get(`/rides/${rideId}/cancel-request`).then(r => r.data), - - raise: (rideId: string, body: CancelRequestBody) => - api.post(`/rides/${rideId}/cancel-request`, body).then(r => r.data), - - approve: (requestId: string) => - api.post(`/rides/cancel-requests/${requestId}/approve`).then(r => r.data), - - reject: (requestId: string) => - api.post(`/rides/cancel-requests/${requestId}/reject`).then(r => r.data), - }, + cancel: (id: string, body: CancelRideBody) => + api.post(`/rides/${id}/cancel`, body).then(r => r.data), } // ─── Loading Sheets ─────────────────────────────────────────────────────────── -export interface LoadingSheetItemBody { - itemId: string - quantityDispatched: number +export interface LoadingSheetCreateBody { + fK_RideId: string } -export interface LoadingSheetBody { - rideId: string - sourceWarehouseId: string - items: LoadingSheetItemBody[] -} - -export interface LoadingSheetReceiptItemBody { - itemId: string - quantityReceived: number +export interface LoadingSheetItemInput { + fK_ItemId: string + fK_WarehouseId: string + quantity: number + fK_ShopPOItemId?: string | null } export interface LoadingSheetListParams extends PageParams { @@ -100,17 +75,14 @@ export const loadingSheets = { get: (id: string) => api.get(`/loading-sheets/${id}`).then(r => r.data), - create: (body: LoadingSheetBody) => + create: (body: LoadingSheetCreateBody) => api.post('/loading-sheets', body).then(r => r.data), - confirmDispatch: (id: string) => - api.patch(`/loading-sheets/${id}/dispatch`).then(r => r.data), + updateItems: (id: string, items: LoadingSheetItemInput[]) => + api.put(`/loading-sheets/${id}/items`, { items }).then(r => r.data), - confirmReceipt: (id: string, items: LoadingSheetReceiptItemBody[]) => - api.patch(`/loading-sheets/${id}/receipt`, { items }).then(r => r.data), - - resolve: (id: string) => - api.patch(`/loading-sheets/${id}/resolve`).then(r => r.data), + confirm: (id: string) => + api.post(`/loading-sheets/${id}/confirm`).then(r => r.data), } // ─── Delivery Tasks ─────────────────────────────────────────────────────────── @@ -121,11 +93,8 @@ export interface DeliveryTaskItemQuantity { } export const deliveryTasks = { - generate: (rideId: string) => - api.post(`/rides/${rideId}/tasks/generate`).then(r => r.data), - listByRide: (rideId: string) => - api.get(`/rides/${rideId}/delivery-tasks`).then(r => r.data), + api.get(`/rides/${rideId}/tasks`).then(r => r.data), get: (id: string) => api.get(`/delivery-tasks/${id}`).then(r => r.data), diff --git a/distribution_fe/lib/api/returns-claims.ts b/distribution_fe/lib/api/returns-claims.ts index f7cea52..bd558af 100644 --- a/distribution_fe/lib/api/returns-claims.ts +++ b/distribution_fe/lib/api/returns-claims.ts @@ -35,11 +35,11 @@ export const saleReturns = { get: (id: string) => api.get(`/returns/${id}`).then(r => r.data), - listByOrder: (orderId: string) => - api.get(`/orders/${orderId}/returns`).then(r => r.data), + listByShopPO: (shopPOId: string) => + api.get(`/shop-pos/${shopPOId}/returns`).then(r => r.data), - create: (orderId: string, body: SaleReturnBody) => - api.post(`/orders/${orderId}/returns`, body).then(r => r.data), + create: (shopPOId: string, body: SaleReturnBody) => + api.post(`/shop-pos/${shopPOId}/returns`, body).then(r => r.data), updateReasons: (id: string, body: UpdateSaleReturnReasonsBody) => api.patch(`/returns/${id}/reasons`, body).then(r => r.data), diff --git a/distribution_fe/lib/api/sales.ts b/distribution_fe/lib/api/sales.ts index 026e920..7d0c280 100644 --- a/distribution_fe/lib/api/sales.ts +++ b/distribution_fe/lib/api/sales.ts @@ -1,101 +1,99 @@ import { api } from '@/lib/api' import type { PaginatedResponse, - SaleResponse, - OrderResponse, - OrderStatus, + ShopPOResponse, + InvoiceResponse, + InvoiceStatus, PaymentResponse, - SaleStatus, - PaymentStatus, PaymentMethod, + ShopPOStatus, } from '@/types/api' import type { PageParams } from './master' -// ─── Sales ──────────────────────────────────────────────────────────────────── +// ─── Shop POs ───────────────────────────────────────────────────────────────── -export interface SaleItemBody { +export interface ShopPOItemBody { itemId: string quantity: number - unitPrice: number + unitPrice?: number | null } -export interface SaleBody { +export interface CreateShopPOBody { storeId: string - saleDate: string - rideId?: string | null - fulfillImmediately?: boolean - items: SaleItemBody[] - // TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28. - createdByUserId: string + items: ShopPOItemBody[] } -export interface SaleListParams extends PageParams { - storeId?: string - status?: SaleStatus - paymentStatus?: PaymentStatus - rideId?: string - from?: string - to?: string +export interface UpdateShopPOItemsBody { + items: ShopPOItemBody[] } -export const sales = { - list: (params?: SaleListParams) => - api.get>('/sales', { params }).then(r => r.data), +export interface ShopPOListParams extends PageParams { + agentId?: string + routeId?: string + shopId?: string + confirmed?: boolean + status?: ShopPOStatus +} + +export const shopPos = { + list: (params?: ShopPOListParams) => + api.get>('/shop-pos', { params }).then(r => r.data), get: (id: string) => - api.get(`/sales/${id}`).then(r => r.data), + api.get(`/shop-pos/${id}`).then(r => r.data), - create: (body: SaleBody) => - api.post('/sales', body).then(r => r.data), + create: (body: CreateShopPOBody) => + api.post('/shop-pos', body).then(r => r.data), + + updateItems: (id: string, body: UpdateShopPOItemsBody) => + api.put(`/shop-pos/${id}/items`, body).then(r => r.data), + + confirm: (id: string) => + api.post(`/shop-pos/${id}/confirm`).then(r => r.data), + + confirmAndCreateInvoice: (id: string) => + api.post(`/shop-pos/${id}/confirm-and-create-invoice`).then(r => r.data), cancel: (id: string) => - api.patch(`/sales/${id}/cancel`).then(r => r.data), + api.post(`/shop-pos/${id}/cancel`).then(r => r.data), } -// ─── Orders ─────────────────────────────────────────────────────────────────── -// GET /orders takes no query params and returns a plain array — no server-side -// filtering/pagination exists on the backend, unlike Sales/Payments. +// ─── Invoices ───────────────────────────────────────────────────────────────── -export const orders = { - list: () => - api.get('/orders').then(r => r.data), +export interface InvoiceListParams extends PageParams { + agentId?: string + routeId?: string + shopId?: string + status?: InvoiceStatus +} + +export const invoices = { + list: (params?: InvoiceListParams) => + api.get>('/invoices', { params }).then(r => r.data), get: (id: string) => - api.get(`/orders/${id}`).then(r => r.data), + api.get(`/invoices/${id}`).then(r => r.data), - update: (id: string, status: OrderStatus) => - api.put(`/orders/${id}`, { status }).then(r => r.data), + print: (id: string) => + api.post(`/invoices/${id}/print`).then(r => r.data), - delete: (id: string) => - api.delete(`/orders/${id}`), + finalize: (id: string) => + api.post(`/invoices/${id}/finalize`).then(r => r.data), } // ─── Payments ───────────────────────────────────────────────────────────────── export interface PaymentBody { - saleId: string amount: number method: PaymentMethod - // TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28. - collectedByUserId: string -} - -export interface PaymentListParams extends PageParams { - storeId?: string - saleId?: string - method?: PaymentMethod - from?: string - to?: string + collectedByUserId?: string | null } export const payments = { - list: (params?: PaymentListParams) => - api.get>('/payments', { params }).then(r => r.data), + listByInvoice: (invoiceId: string) => + api.get(`/invoices/${invoiceId}/payments`).then(r => r.data), - get: (id: string) => - api.get(`/payments/${id}`).then(r => r.data), - - create: (body: PaymentBody) => - api.post('/payments', body).then(r => r.data), + create: (invoiceId: string, body: PaymentBody) => + api.post(`/invoices/${invoiceId}/payments`, body).then(r => r.data), } diff --git a/distribution_fe/types/api.ts b/distribution_fe/types/api.ts index 5f00b9e..d8f799a 100644 --- a/distribution_fe/types/api.ts +++ b/distribution_fe/types/api.ts @@ -5,19 +5,15 @@ export type LocationType = 'Warehouse' | 'Vehicle' | 'External' export type MovementType = | 'Sale' | 'CustomerReturn' | 'Transfer' | 'Damage' | 'Adjustment' | 'Load' | 'PurchaseReceipt' | 'PurchaseReturn' -export type LoadingSheetStatus = - | 'Pending' | 'DispatchConfirmed' | 'ReceiptConfirmed' - | 'DiscrepancyFlagged' | 'Resolved' -export type RideType = 'Delivery' | 'Sale' -export type RideStatus = - | 'Draft' | 'Open' | 'Active' | 'CancelRequested' | 'Cancelled' | 'Closed' +export type LoadingSheetStatus = 'Planned' | 'Confirmed' +export type RideStatus = 'Open' | 'Cancelled' | 'Closed' export type CancelRequestType = | 'VehicleBreakdown' | 'DriverUnavailable' | 'RouteIssue' | 'Other' -export type CancelRequestStatus = 'Pending' | 'Approved' | 'Rejected' export type PurchaseOrderStatus = | 'Issued' | 'PartiallyReceived' | 'Received' | 'Closed' | 'Cancelled' -export type SaleStatus = 'Pending' | 'Completed' | 'Cancelled' -export type OrderStatus = 'Pending' | 'PartiallyFulfilled' | 'Fulfilled' | 'Cancelled' +export type ShopPOStatus = 'Pending' | 'PartiallyFulfilled' | 'Fulfilled' | 'Cancelled' +export type InvoiceStatus = 'Draft' | 'Printed' | 'Finalized' +export type InvoiceCreationMode = 'ModeA' | 'ModeB' export type PaymentStatus = 'Pending' | 'PartiallyPaid' | 'Paid' export type PaymentMethod = 'Cash' | 'Credit' export type DeliveryTaskStatus = 'Pending' | 'Delivered' | 'PartiallyDelivered' | 'Failed' @@ -457,7 +453,7 @@ export interface SaleReturnItemResponse { export interface SaleReturnResponse { id: string - orderId: string + shopPOId: string mode: ReturnMode totalCreditAmount: number orderTotalAmountAfter: number @@ -555,59 +551,45 @@ export interface DriverComplianceConfigResponse { export interface RideResponse { id: string - rideType: RideType status: RideStatus vehicleId: string vehicleNumberPlate: string driverProfileId: string driverFullName: string - routeId: string - routeName: string - sourceWarehouseId: string - sourceWarehouseName: string meterstart: number meterend: number startedAt: string | null - activatedAt: string | null closedAt: string | null createdAt: string } -export interface CancelRequestResponse { - id: string - rideId: string - typeCode: CancelRequestType - reason: string - requestedBy: string - requestedAt: string - status: CancelRequestStatus - approvedBy: string | null - approvedAt: string | null -} - // ─── Loading Sheets ─────────────────────────────────────────────────────────── export interface LoadingSheetItemResponse { id: string fK_ItemId: string itemName: string + fK_WarehouseId: string + warehouseName: string quantityDispatched: number - quantityReceived: number - quantityResolved: number + fK_ShopPOItemId: string | null +} + +export interface LoadingSheetOrderRef { + shopPOId: string } export interface LoadingSheetResponse { id: string fK_RideId: string - fK_SourceWarehouseId: string - warehouseName: string status: LoadingSheetStatus - dispatchConfirmedAt: string | null - dispatchConfirmedBy: string | null - receiptConfirmedAt: string | null - receiptConfirmedBy: string | null + populatedBy: string + populatedAt: string + confirmedAt: string | null + confirmedBy: string | null createdAt: string items: LoadingSheetItemResponse[] + orders: LoadingSheetOrderRef[] } // ─── Delivery Tasks ─────────────────────────────────────────────────────────── @@ -623,7 +605,7 @@ export interface DeliveryTaskItemResponse { export interface DeliveryTaskResponse { id: string fK_RideId: string - fK_OrderId: string + fK_ShopPOId: string storeId: string storeName: string sequence: number @@ -635,58 +617,53 @@ export interface DeliveryTaskResponse { createdAt: string } -// ─── Sales / Orders / Payments ──────────────────────────────────────────────── +// ─── ShopPO / Invoice / Payment ─────────────────────────────────────────────── -export interface SaleItemResponse { +export interface ShopPOItemResponse { + id: string itemId: string + itemName: string quantity: number quantityFulfilled: number unitPrice: number + isActive: boolean } -export interface SaleResponse { +export interface ShopPOResponse { id: string - saleDate: string storeId: string - createdByUserId: string - rideId: string | null + storeName: string + raisedById: string + invoiceId: string | null totalAmount: number - status: SaleStatus - paymentStatus: PaymentStatus - fulfillImmediately: boolean - completedAt: string | null - orderId: string - orderStatus: OrderStatus - orderFulfilledAt: string | null - items: SaleItemResponse[] + status: ShopPOStatus + confirmedAt: string | null + confirmedBy: string | null + fulfilledAt: string | null + items: ShopPOItemResponse[] isActive: boolean createdAt: string } -export interface OrderItemResponse { - itemId: string - quantity: number - quantityFulfilled: number - unitPrice: number -} - -export interface OrderResponse { +export interface InvoiceResponse { id: string - saleId: string - orderDate: string - storeId: string + shopPOId: string + storeName: string + status: InvoiceStatus + paymentStatus: PaymentStatus + createdBy: string + printedAt: string | null + printedBy: string | null + finalizedAt: string | null + finalizedBy: string | null totalAmount: number - status: OrderStatus - fulfilledAt: string | null - items: OrderItemResponse[] isActive: boolean createdAt: string } export interface PaymentResponse { id: string - saleId: string - storeId: string + invoiceId: string amount: number method: PaymentMethod collectedAt: string @@ -694,6 +671,13 @@ export interface PaymentResponse { createdAt: string } +export interface SysConfigResponse { + key: string + value: string + updatedAt: string + updatedBy: string | null +} + // ─── API Error ──────────────────────────────────────────────────────────────── export interface ApiError {