feat: added tab products to manager the client price
parent
d747e063d7
commit
f9786de1b7
|
@ -19,6 +19,7 @@ flask-jwt-extended = "*"
|
|||
flask-bcrypt = "*"
|
||||
flask-cors = "*"
|
||||
mypy = "*"
|
||||
requests = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "41c45852f7940e8cbf541673b9e42d7a8b139527a30b88edad5b6620405e117a"
|
||||
"sha256": "3798a8dec1f82bd3b8d8e6089e8110ae5e152e1e76d9b78810ba5b4cfab33c2f"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -104,6 +104,112 @@
|
|||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6",
|
||||
"sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2025.4.26"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4",
|
||||
"sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45",
|
||||
"sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7",
|
||||
"sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0",
|
||||
"sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7",
|
||||
"sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d",
|
||||
"sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d",
|
||||
"sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0",
|
||||
"sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184",
|
||||
"sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db",
|
||||
"sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b",
|
||||
"sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64",
|
||||
"sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b",
|
||||
"sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8",
|
||||
"sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff",
|
||||
"sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344",
|
||||
"sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58",
|
||||
"sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e",
|
||||
"sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471",
|
||||
"sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148",
|
||||
"sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a",
|
||||
"sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836",
|
||||
"sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e",
|
||||
"sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63",
|
||||
"sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c",
|
||||
"sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1",
|
||||
"sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01",
|
||||
"sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366",
|
||||
"sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58",
|
||||
"sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5",
|
||||
"sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c",
|
||||
"sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2",
|
||||
"sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a",
|
||||
"sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597",
|
||||
"sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b",
|
||||
"sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5",
|
||||
"sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb",
|
||||
"sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f",
|
||||
"sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0",
|
||||
"sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941",
|
||||
"sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0",
|
||||
"sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86",
|
||||
"sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7",
|
||||
"sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7",
|
||||
"sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455",
|
||||
"sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6",
|
||||
"sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4",
|
||||
"sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0",
|
||||
"sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3",
|
||||
"sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1",
|
||||
"sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6",
|
||||
"sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981",
|
||||
"sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c",
|
||||
"sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980",
|
||||
"sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645",
|
||||
"sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7",
|
||||
"sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12",
|
||||
"sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa",
|
||||
"sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd",
|
||||
"sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef",
|
||||
"sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f",
|
||||
"sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2",
|
||||
"sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d",
|
||||
"sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5",
|
||||
"sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02",
|
||||
"sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3",
|
||||
"sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd",
|
||||
"sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e",
|
||||
"sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214",
|
||||
"sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd",
|
||||
"sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a",
|
||||
"sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c",
|
||||
"sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681",
|
||||
"sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba",
|
||||
"sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f",
|
||||
"sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a",
|
||||
"sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28",
|
||||
"sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691",
|
||||
"sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82",
|
||||
"sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a",
|
||||
"sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027",
|
||||
"sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7",
|
||||
"sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518",
|
||||
"sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf",
|
||||
"sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b",
|
||||
"sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9",
|
||||
"sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544",
|
||||
"sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da",
|
||||
"sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509",
|
||||
"sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f",
|
||||
"sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a",
|
||||
"sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.4.2"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202",
|
||||
|
@ -154,12 +260,12 @@
|
|||
},
|
||||
"flask-cors": {
|
||||
"hashes": [
|
||||
"sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393",
|
||||
"sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657"
|
||||
"sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c",
|
||||
"sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9' and python_version < '4.0'",
|
||||
"version": "==6.0.0"
|
||||
"version": "==6.0.1"
|
||||
},
|
||||
"flask-jwt-extended": {
|
||||
"hashes": [
|
||||
|
@ -247,6 +353,14 @@
|
|||
"markers": "python_version < '3.14' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
|
||||
"version": "==3.2.3"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
|
||||
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.10"
|
||||
},
|
||||
"importlib-resources": {
|
||||
"hashes": [
|
||||
"sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c",
|
||||
|
@ -680,67 +794,67 @@
|
|||
},
|
||||
"pymongo": {
|
||||
"hashes": [
|
||||
"sha256:007450b8c8d17b4e5b779ab6e1938983309eac26b5b8f0863c48effa4b151b07",
|
||||
"sha256:02f0e1af87280697a1a8304238b863d4eee98c8b97f554ee456c3041c0f3a021",
|
||||
"sha256:1397eac713b84946210ab556666cfdd787eee824e910fbbe661d147e110ec516",
|
||||
"sha256:1bac84ee40032bec4c089e92970893157fcd0ef40b81157404ceb4c1dac8ba72",
|
||||
"sha256:209efd3b62cdbebd3cc7a76d5e37414ad08c9bfe8b28ae73695ade065d5b1277",
|
||||
"sha256:267eff6a66da5cf5255b3bcd257984619e9c4d41a53578d4e1d827553a51cf40",
|
||||
"sha256:2d377bb0811e0a9676bacb21a4f87ef307f2e9a40a625660c113a9c0ae897e8c",
|
||||
"sha256:3d631d879e934b46222f5092d8951cbb9fe83542649697c8d342ea7b5479f118",
|
||||
"sha256:3f33b8c1405d05517dce06756f2800b37dd098216cae5903cd80ad4f0a9dad08",
|
||||
"sha256:46c8bce9af98556110a950939f3eaa3f7648308d60df65feb783c780f8b9bfa9",
|
||||
"sha256:50c503b7e809e54740704ec4c87a0f2ccdb910c3b1d36c07dbd2029b6eaa6a50",
|
||||
"sha256:51081910a91e3451db74b7265ee290c72220412aa8897d6dfe28f6e5d80b685b",
|
||||
"sha256:5303e2074b85234e337ebe622d353ce38a35696cd47a7d970f84b545288aee01",
|
||||
"sha256:5adc1349fd5c94d5dfbcbd1ad9858d1df61945a07f5905dcf17bb62eb4c81f93",
|
||||
"sha256:5dea2f6b44697eda38a11ef754d2adfff5373c51b1ffda00b9fedc5facbd605f",
|
||||
"sha256:61733c8f1ded90ab671a08033ee99b837073c73e505b3b3b633a55a0326e77f4",
|
||||
"sha256:6208b83e7d566935218c0837f3b74c7d2dda83804d5d843ce21a55f22255ab74",
|
||||
"sha256:66800de4f4487e7c437991b44bc1e717aadaf06e67451a760efe5cd81ce86575",
|
||||
"sha256:6a8f060f8ad139d1d45f75ef7aa0084bd7f714fc666f98ef00009efc7db34acd",
|
||||
"sha256:6b91f59137e46cd3ff17d5684a18e8006d65d0ee62eb1068b512262d1c2c5ae8",
|
||||
"sha256:81b46d9bc62128c3d968336f8635bcfce33d8e9e1fc6be6ebdfb98effaccb9c7",
|
||||
"sha256:82c36928c1c26580ce4f2497a6875968636e87c77108ff253d76b1355181a405",
|
||||
"sha256:899a5ea9cd32b1b0880015fdceaa36a41140a8c2ce8621626c52f7023724aed6",
|
||||
"sha256:8af08ba2886f08d334bc7e5d5c662c60ea2f16e813a2c35106f399463fa11087",
|
||||
"sha256:8e11ea726ff8ddc8c8393895cd7e93a57e2558c27273d3712797895c53d25692",
|
||||
"sha256:8e90195cb5aee24a67a29adde54c1dae4d9744e17e4585bea3a83bfff96db46c",
|
||||
"sha256:92a06e3709e3c7e50820d352d3d4e60015406bcba69808937dac2a6d22226fde",
|
||||
"sha256:92f5e75ae265e798be1a8a40a29e2ab934e156f3827ca0e1c47e69d43f4dcb31",
|
||||
"sha256:936f7be9ed6919e3be7369b858d1c58ebaa4f3ef231cf4860779b8ba3b4fcd11",
|
||||
"sha256:99a52cfbf31579cc63c926048cd0ada6f96c98c1c4c211356193e07418e6207c",
|
||||
"sha256:a9fe172e93551ddfdb94b9ad34dccebc4b7b680dc1d131bc6bd661c4a5b2945c",
|
||||
"sha256:b2afe49109b4d498d8e55ac9692915f2a3fce0bd31646bb7ed41f9ab3546ca19",
|
||||
"sha256:b54e19e0f6c8a7ad0c5074a8cbefb29c12267c784ceb9a1577a62bbc43150161",
|
||||
"sha256:b63d9d8be87f4be11972c5a63d815974c298ada59a2e1d56ef5b6984d81c544a",
|
||||
"sha256:b9288188101506a9d1aa3f70f65b7f5f499f8f7d5c23ec86a47551d756e32059",
|
||||
"sha256:bd0c9322fdf1b9e8a5c99ca337bd9a99d972ba57c976e77b5017366ba26725e1",
|
||||
"sha256:bd23119f9d0358aa1f78174d2eda88ca5c882a722e25ca31197402278acddc6e",
|
||||
"sha256:be048fb78e165243272a8cdbeb40d53eace82424b95417ab3ab6ec8e9b00c59b",
|
||||
"sha256:c02160ab3a67eca393a2a2bb83dccddf4db2196d0d7c6a980a55157e4bdadc06",
|
||||
"sha256:c03e02129ad202d8e146480b398c4a3ea18266ee0754b6a4805de6baf4a6a8c7",
|
||||
"sha256:c7d740560710be0c514bc9d26f5dcbb3c85dbb6b450c4c3246d8136ca84055bd",
|
||||
"sha256:cef461fae88ac51cd6b3f81adf58171113c58c0e77c82c751b3bdcef516cfeb1",
|
||||
"sha256:d10d3967e87c21869f084af5716d02626a17f6f9ccc9379fcbece5821c2a9fb4",
|
||||
"sha256:d4b4942e5566a134fe34c03d7182a0b346e4a478defe625dc430dd5a178ad96e",
|
||||
"sha256:d684d9b385d97ab821d2ae74628c81a8bd12a4e5004a3ded0ec8c20381d62d0e",
|
||||
"sha256:d81d159bd23d8ac53a6e819cccee991cb9350ab2541dfaa25aeb2f712d23b0a5",
|
||||
"sha256:d842e11eb94f7074314ff1d97a05790539a1d74c3048ce50ea9f0da1f4f96b0a",
|
||||
"sha256:d9a1d7d49d0d364520894116133d017b6e0e2d5131eb31c8553552fa77a65085",
|
||||
"sha256:dc9e412911f210d9b0eca42d25c22d3725809dda03dedbaf6f9ffa192d461905",
|
||||
"sha256:e4a7855933011026898ea0d4532fbd83cef63a76205c823a4ef5557d970df1f1",
|
||||
"sha256:e7d349066f4c229d638a30f1f53ec3a4aaf4a4fc568491bdf77e7415a96003fb",
|
||||
"sha256:ea47a64ed9918be0fa8a4a11146a80f546c09e0d65fd08e90a5c00366a59bdb0",
|
||||
"sha256:f0b26cd4e090161927b7a81741a3627a41b74265dfb41c6957bfb474504b4b42",
|
||||
"sha256:f39791a88cd5ec1760f65e878af419747c6f94ce74f9293735cbba6025ff4d0d",
|
||||
"sha256:fb780d9d284ffdf7922edd4a6d7ba08e54a6680f85f64f91fa9cc2617dd488b7",
|
||||
"sha256:fca24e4df05501420b2ce2207c03f21fcbdfac1e3f41e312e61b8f416c5b4963",
|
||||
"sha256:fe497c885b08600a022646f00f4d3303697c5289990acec250e2be2e1699ca23"
|
||||
"sha256:01d4b136e2e71c1ecf20cd38222eea154b347b4230eae1db2fa0a4a81ae824cd",
|
||||
"sha256:047fde40672831d546fde0f57b494556db1467283864e0faf10a109a87b5153d",
|
||||
"sha256:08dd70224978831ce7dc76016205d9b56c86aa337077f65a54b672240e7e9d6d",
|
||||
"sha256:0a9db585de9622ea0834833c4d48b873f83d10ec31c7370fb92c87d5f4e1b805",
|
||||
"sha256:0d46d18854fed2196ea85fff3ee985c89658dd8c090292e6b174aa1387fe5dc4",
|
||||
"sha256:113fc90ea0c7fd6dc288844c325cab76fce06dfccecf45eac55c384a167abad4",
|
||||
"sha256:1ab8f8272f882e24e45772f20134bd0e2ad31b08153a068cf812c377088bb3b1",
|
||||
"sha256:2c49a8274e79093697f274946e9e78079edf504158790401b538ba417c1cae5d",
|
||||
"sha256:3078a59ce15625a91963afc5086c69995877df10ea261bb82d099a1bbbe80efe",
|
||||
"sha256:43f618d956b8f97e2483452d0a49d652bb07088c34cbf00917de6d61d31df4f4",
|
||||
"sha256:443de2fd340f0b899a59d5b4fd4c773f016e8cbed8075975783bced3583fa0a8",
|
||||
"sha256:47cc337065de00c087260aa8e8b91e1a3688b69cd1b181a478c1e84cf248d41d",
|
||||
"sha256:493c162b783614c69efdfc41ebe9521df6cc5da6be3d25c92535db111e804952",
|
||||
"sha256:494ce1f93d507e8e170886dbefbef32662ac0cfe5b1734a65b6e523103b7ef3c",
|
||||
"sha256:4e314f863a338c1c716788d3b3a7be566cca8b4351aa637aefa9c2609d1c643e",
|
||||
"sha256:4fad51a0d951523e5f43d8271d074b700a64f46b347da176646816b9e1540a44",
|
||||
"sha256:59ad9f47a95bd330e423fa506714494d84f1af6b4095f3a30f7d69f0ed741d93",
|
||||
"sha256:59f5b845773a1d20c6808656a4ca63975f790e3fd5343a4a72ca1506ab852258",
|
||||
"sha256:5a8ca3e3383b2f33fe765e114af37211c3ba2a4654745756a94aa322b1c52523",
|
||||
"sha256:5f69576ccb93c1859075493a7890f9a3b3a89fd46427745ce08bc82100d3f60d",
|
||||
"sha256:6109e0dd1c814534a0c583a59d7a2e2210b6a0ee2dba98dc6589ad2b53e20f84",
|
||||
"sha256:6249560e51a6e881cca6258d2de1241d30f50d45d8c7fc45205e1776306f44d0",
|
||||
"sha256:629026d35e41f069a8d49347ff76c7c205cd97b62537da7498b008ceba542c85",
|
||||
"sha256:643b79645944899b2addb4ce03d9a6718dd1f4a0e1fa71fecdc49e6dea072b62",
|
||||
"sha256:6492565cd7bb10cb6104401af446926141249095953b57c108c4bdcf3452fa3d",
|
||||
"sha256:65702467612d338028551fa0f6b3107ef48280d9a21f4cadba0128ee6eea82da",
|
||||
"sha256:6823c2783c5da1bb0dc9759387db4f8ac266563f42fdb6709d6b15ca5a7e06b7",
|
||||
"sha256:6cec4206aac402f5097ee5d3f620c0e596f541f4872e91b3c936174d5dcc2d01",
|
||||
"sha256:6fb3d549a735c77470187d84d2087495be96cf86ce1dfc91ba3f69bf2abeded2",
|
||||
"sha256:716584c6af2e849f219da87decda6ac52f786a388f704af9cadc94133dc054e2",
|
||||
"sha256:71a6640278af8bc13c9272fdd2df90e62e2cdad8e864ffb04d1a8c7dda0131c1",
|
||||
"sha256:759c879af24af5a4256e3675180b2d392bb7f009ccf1693fc12af66d38aadea7",
|
||||
"sha256:77f5c0d57f381c76f143b951ac8e879d5ea57f7e51a3ecdd906a0d2cd3eb25e1",
|
||||
"sha256:784077af7e4c35987132dfe8ee559041bdb07bde17c95cd507275b26cb5c2742",
|
||||
"sha256:78f86db7b94891ba61adbe00d4cf6eeead51bac2fb0cf6b8ec7c954236fe1ca3",
|
||||
"sha256:7b0d614275cef7457fb0c9ae5831b35361bbb826f09ceb7d289f78c69f8bad45",
|
||||
"sha256:84f595bc1c1e8c7c784f52e6803540b8963df5ed561bfc2caa8b79111d59e987",
|
||||
"sha256:86138e17f67fc959b609cf449ddcd3089acdb75d4b1c42a0d2b258128418f5a0",
|
||||
"sha256:865f76463744169f44f49747ec3b01120a8c98c6d635fb12a5f5a86fcb0a462d",
|
||||
"sha256:8e994e4e99980a87b73be43b27820d20ff823eb966abd01f2242b71c0723d685",
|
||||
"sha256:90189695c019bf9ea1c701dea20f922bb6c11d0379ebfef4894e227bbac1f67c",
|
||||
"sha256:906db7bbe127bc295a1482226b4ebf55d3f6442424ad5e64f2db164d03e794dd",
|
||||
"sha256:9318befdd8b78c2c4743ab42a86c6b6783209716aa1c5260b3517c6eea8b934c",
|
||||
"sha256:99eeb43a318558fe074f88ffaad4ac027ef2a02cdce4f81602a5fbb1332908d0",
|
||||
"sha256:a01b02180c6cfb6103cb8a27f2bbd7e017c7e2b3c5e34894c614aa8e63ee17df",
|
||||
"sha256:a2e557dedf2ca26a6ad6a6d41003177de113606256754f70f75d3e6902ae26e9",
|
||||
"sha256:a56960e5e7c2972332879ca5b6bd6bb39df0e344cba3d3cf4513fa2dfce54a1f",
|
||||
"sha256:aad7474fc4b35265bab66b54319d31a7aae948095f8b03ea883ea4997fac98e8",
|
||||
"sha256:ad6256e8157505052e3b7733e4b0c6038244ff082715f3a2ad8289ccf7ba16ac",
|
||||
"sha256:bdbfb7828f06f3f1a928abd3e4c67487167e2c85a5598194157edfc732f3a571",
|
||||
"sha256:c899acff9404b004ca7ba06ff7e30fb38ddf527431d75a5af6cb1133365c9a40",
|
||||
"sha256:df0233ebfab9fe6f9cd41f5931261f53deee0e0b2e0b0fc5a515dc54cf39d789",
|
||||
"sha256:eb6c206ad5d3e9e33ed84495e9b1b87af37590256d3274ee62749ac43a5fe2b1",
|
||||
"sha256:ebf010ad764c81f0f7f85777c9e6684f2bdac65ff113b88a98946826e7318df0",
|
||||
"sha256:f2d58b020d8c71eb9456cb435f355ff0b578259c0cf5a79ca3efab386ebb71de",
|
||||
"sha256:fab03d9a4107d46864a9797dfedb0645b2dd97dfb2b1ba582e471c0c4e692ebe",
|
||||
"sha256:fc7ff2857fb3e9d865230f4226e57045daa9b09bdb21e4c6809f438e85f86fff"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==4.13.0"
|
||||
"version": "==4.13.1"
|
||||
},
|
||||
"pymysql": {
|
||||
"hashes": [
|
||||
|
@ -800,6 +914,15 @@
|
|||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.36.2"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c",
|
||||
"sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.32.4"
|
||||
},
|
||||
"rpds-py": {
|
||||
"hashes": [
|
||||
"sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d",
|
||||
|
@ -1018,6 +1141,14 @@
|
|||
"markers": "python_version >= '2'",
|
||||
"version": "==2025.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466",
|
||||
"sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e",
|
||||
|
|
|
@ -5,6 +5,7 @@ from .extensions import init_mongo, init_jwt
|
|||
from .errors.handlers import register_error_handlers
|
||||
from .routes.usage_routes import usage_ns
|
||||
from .routes.auth_routes import auth_ns
|
||||
from .routes.billing_routes import billing_ns
|
||||
from flask_cors import CORS
|
||||
|
||||
def create_app():
|
||||
|
@ -35,6 +36,7 @@ def create_app():
|
|||
|
||||
api.add_namespace(usage_ns, path='/usage')
|
||||
api.add_namespace(auth_ns, path='/auth')
|
||||
api.add_namespace(billing_ns, path='/billing')
|
||||
|
||||
register_error_handlers(app)
|
||||
|
||||
|
|
|
@ -23,6 +23,9 @@ class Config:
|
|||
|
||||
FRONTEND_URL = os.getenv("FRONTEND_URL", 3000)
|
||||
|
||||
BILLING_API_URL = os.getenv("BILLING_API_URL")
|
||||
BILLING_API_TOKEN = os.getenv("BILLING_API_TOKEN")
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from flask_restx import fields, Namespace
|
||||
|
||||
billing_ns = Namespace('billing', description='Authentication')
|
||||
|
||||
product_model = billing_ns.model('Product', {
|
||||
'name': fields.String(required=True, description='Name of the product'),
|
||||
'description': fields.String(required=True, description='Description of the product'),
|
||||
'price': fields.Float(required=True, description='Price of the product'),
|
||||
})
|
||||
|
||||
update_price_model = billing_ns.model('UpdatePrice', {
|
||||
'price': fields.Float(required=False, description='New product price'),
|
||||
'description': fields.String(required=False, description='New description of the product'),
|
||||
'price': fields.Float(required=False, description='New price of the product'),
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
from flask_restx import Namespace, Resource, fields
|
||||
from flask import request, current_app
|
||||
import requests
|
||||
from bson import json_util
|
||||
from app.db.models import UserModel
|
||||
from app.docs.biling_models import billing_ns, product_model, update_price_model
|
||||
from app.config import Config
|
||||
|
||||
|
||||
BILLING_API_URL = Config.BILLING_API_URL
|
||||
HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {Config.BILLING_API_TOKEN}'
|
||||
}
|
||||
|
||||
|
||||
@billing_ns.route('/product')
|
||||
class CreateProduct(Resource):
|
||||
@billing_ns.expect(product_model)
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
|
||||
response = requests.post(url=f'{BILLING_API_URL}/billing/product', json=data, headers=HEADERS)
|
||||
return response.json(), response.status_code
|
||||
|
||||
|
||||
@billing_ns.route('/product/<string:product_id>')
|
||||
class UpdateProduct(Resource):
|
||||
@billing_ns.expect(update_price_model)
|
||||
def patch(self, product_id):
|
||||
data = request.get_json()
|
||||
|
||||
response = requests.patch(url=f'{BILLING_API_URL}/billing/product/{product_id}', json=data, headers=HEADERS)
|
||||
return response.json(), response.status_code
|
||||
|
||||
|
||||
@billing_ns.route('/products')
|
||||
class ListProducts(Resource):
|
||||
def get(self):
|
||||
response = requests.get(url=f'{BILLING_API_URL}/billing/products', headers=HEADERS)
|
||||
return response.json(), response.status_code
|
|
@ -10,6 +10,7 @@ import os
|
|||
from typing import List, Dict, Any, Optional
|
||||
from app.utils.mysql_query import execute_query
|
||||
from app.utils.calc_api_usage import calculate_api_usage
|
||||
import math
|
||||
|
||||
|
||||
class TranscriptionReportService:
|
||||
|
@ -24,8 +25,11 @@ class TranscriptionReportService:
|
|||
|
||||
|
||||
def _fetch_mongo_data(self, page: int = 1, page_size: int = 20, all_data: Optional[bool] = False) -> Dict[str, int]:
|
||||
collection = self.mongo_client["billing-api"]["api_pricings"]
|
||||
result_stt = collection.find({"type": "stt"})
|
||||
collection = self.mongo_client["billing-api"]["api_usages"]
|
||||
|
||||
# Define os produtos válidos a partir da tabela de preços
|
||||
pricing_collection = self.mongo_client["billing-api"]["api_pricings"]
|
||||
result_stt = pricing_collection.find({"product": {"$nin": ["whatsapp"]}})
|
||||
products = [t["product"] for t in result_stt]
|
||||
|
||||
match_stage = {
|
||||
|
@ -39,55 +43,129 @@ class TranscriptionReportService:
|
|||
}
|
||||
}
|
||||
|
||||
group_stage = {
|
||||
lookup_stage = {
|
||||
"$lookup": {
|
||||
"from": "api_pricings",
|
||||
"localField": "product",
|
||||
"foreignField": "product",
|
||||
"as": "pricing"
|
||||
}
|
||||
}
|
||||
|
||||
unwind_stage = {
|
||||
"$unwind": "$pricing"
|
||||
}
|
||||
|
||||
# Agrupa por sessionId + type + product
|
||||
group_stage_1 = {
|
||||
"$group": {
|
||||
"_id": "$sessionId",
|
||||
"count": {"$sum": 1},
|
||||
"firstCreatedAt": {"$first": "$createdAt"},
|
||||
"callerIds": {"$addToSet": "$callerId"},
|
||||
"_id": {
|
||||
"sessionId": "$sessionId",
|
||||
"type": "$pricing.type",
|
||||
"product": "$product"
|
||||
},
|
||||
"usage": {"$sum": {"$toDouble": "$usage"}},
|
||||
"totalCost": {"$sum": {"$toDouble": "$total_cost"}},
|
||||
"totalUsage": {"$sum": {"$toDouble": "$usage"}},
|
||||
"price": {"$first": {"$toDouble": "$price"}},
|
||||
"product": {"$first": "$product"}
|
||||
"callerIds": {"$addToSet": "$callerId"},
|
||||
"firstCreatedAt": {"$min": "$createdAt"}
|
||||
}
|
||||
}
|
||||
|
||||
# Agrupa por sessionId final, montando maps de uso e custo
|
||||
group_stage_2 = {
|
||||
"$group": {
|
||||
"_id": "$_id.sessionId",
|
||||
"count": {"$sum": 1},
|
||||
"firstCreatedAt": {"$first": "$firstCreatedAt"},
|
||||
"callerIds": {"$first": "$callerIds"},
|
||||
"totalCost": {"$sum": "$totalCost"},
|
||||
"usageByType": {
|
||||
"$push": {
|
||||
"k": "$_id.type",
|
||||
"v": "$usage"
|
||||
}
|
||||
},
|
||||
"costByType": {
|
||||
"$push": {
|
||||
"k": "$_id.type",
|
||||
"v": "$totalCost"
|
||||
}
|
||||
},
|
||||
"usageByProduct": {
|
||||
"$push": {
|
||||
"k": "$_id.product",
|
||||
"v": "$usage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project_stage = {
|
||||
"$project": {
|
||||
"count": 1,
|
||||
"firstCreatedAt": 1,
|
||||
"callerIds": 1,
|
||||
"totalCost": 1,
|
||||
"usageByType": {"$arrayToObject": "$usageByType"},
|
||||
"costByType": {"$arrayToObject": "$costByType"},
|
||||
"usageByProduct": {"$arrayToObject": "$usageByProduct"}
|
||||
}
|
||||
}
|
||||
|
||||
sort_stage = {"$sort": {"firstCreatedAt": 1}}
|
||||
|
||||
pipeline = [match_stage, group_stage, sort_stage]
|
||||
# Monta o pipeline
|
||||
pipeline = [
|
||||
match_stage,
|
||||
lookup_stage,
|
||||
unwind_stage,
|
||||
group_stage_1,
|
||||
group_stage_2,
|
||||
project_stage,
|
||||
sort_stage
|
||||
]
|
||||
|
||||
if not all_data:
|
||||
# Aplica paginação se all_data for False
|
||||
pipeline.extend([
|
||||
{"$skip": (page - 1) * page_size},
|
||||
{"$limit": page_size}
|
||||
])
|
||||
|
||||
# Executa pipeline com ou sem paginação
|
||||
collection = self.mongo_client["billing-api"]["api_usages"]
|
||||
# Executa agregação principal
|
||||
self.mongo_results = list(collection.aggregate(pipeline))
|
||||
self.unique_ids = [doc["_id"] for doc in self.mongo_results]
|
||||
|
||||
# print("=====> mongoResults: ", self.mongo_results)
|
||||
# print("=====> self.mongo_results: ", self.mongo_results)
|
||||
|
||||
# Sempre calcula o total (para controle)
|
||||
# Pipeline para contagem total
|
||||
count_pipeline = [
|
||||
match_stage,
|
||||
group_stage,
|
||||
lookup_stage,
|
||||
unwind_stage,
|
||||
group_stage_1,
|
||||
group_stage_2,
|
||||
{"$count": "total"}
|
||||
]
|
||||
count_result = list(collection.aggregate(count_pipeline))
|
||||
total = count_result[0]["total"] if count_result else 0
|
||||
|
||||
return {
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _fetch_mysql_data(self, hit_report: Optional[bool] = False)-> List[Dict[str, Any]]:
|
||||
|
||||
|
||||
|
||||
def _fetch_mysql_data(self, hit_report: Optional[bool] = False)-> List[Dict[str, Any]]:
|
||||
|
||||
collection = self.mongo_client["billing-api"]["api_products"]
|
||||
|
||||
products = list(collection.find({}))
|
||||
|
||||
sql = f"""SELECT
|
||||
uniqueid,
|
||||
src,
|
||||
|
@ -108,57 +186,85 @@ class TranscriptionReportService:
|
|||
rows = execute_query(self.company_id, sql)
|
||||
|
||||
if hit_report:
|
||||
collection = self.mongo_client["billing-api"]["api_pricings"]
|
||||
result_stt = collection.find({"type": "stt"})
|
||||
# collection = self.mongo_client["billing-api"]["api_pricings"]
|
||||
# result_stt = collection.find({"type": "stt"})
|
||||
|
||||
result_stt = [{"product": t["product"], "clientPrice": t["clientPrice"]} for t in result_stt if "clientPrice" in t]
|
||||
# result_stt = [{"product": t["product"], "clientPrice": t["clientPrice"]} for t in result_stt if "clientPrice" in t]
|
||||
|
||||
for row in rows:
|
||||
for row in rows:
|
||||
row["companyId"] = self.company_id
|
||||
|
||||
if rowMongo := next((m for m in self.mongo_results if m["_id"] == row["uniqueid"] ), None):
|
||||
row["custo_HIT"] = rowMongo["totalCost"]
|
||||
row["price"] = rowMongo["price"]
|
||||
|
||||
p = [p for p in result_stt if p['product'] == rowMongo["product"]]
|
||||
|
||||
if len(p) > 0:
|
||||
row["client_price"] = p[0]["clientPrice"]
|
||||
# row["client_total_cost"] = calculate_api_usage(float(p[0]["clientPrice"]), 60, float(rowMongo["totalUsage"]))
|
||||
row["custo_hit"] = f"{float(rowMongo["totalCost"])}"
|
||||
row["qtd_token_input"] = rowMongo.get('usageByType', {}).get('input', 0)
|
||||
row["qtd_token_output"] = rowMongo.get('usageByType', {}).get('output', 0)
|
||||
|
||||
|
||||
for key in row:
|
||||
if isinstance(row[key], datetime):
|
||||
row[key] = row[key].isoformat(sep=' ')
|
||||
elif isinstance(row[key], Decimal):
|
||||
row[key] = float(row[key])
|
||||
elif key == "uniqueid":
|
||||
row[key] = str(row[key])
|
||||
self.client_price_row(products, row)
|
||||
self.formate_properties(row)
|
||||
|
||||
else:
|
||||
for row in rows:
|
||||
row["total_min"] = f"{(int(row['total_billsec']) / 60):.2f}"
|
||||
del row["end_call"]
|
||||
for key in row:
|
||||
if isinstance(row[key], datetime):
|
||||
row[key] = row[key].isoformat(sep=' ')
|
||||
elif isinstance(row[key], Decimal):
|
||||
row[key] = float(row[key])
|
||||
elif key == "uniqueid":
|
||||
row[key] = str(row[key])
|
||||
|
||||
self.client_price_row(products, row)
|
||||
self.formate_properties(row)
|
||||
|
||||
return rows
|
||||
|
||||
def formate_properties(self, row):
|
||||
for key in row:
|
||||
if isinstance(row[key], datetime):
|
||||
row[key] = row[key].isoformat(sep=' ')
|
||||
elif isinstance(row[key], Decimal):
|
||||
row[key] = float(row[key])
|
||||
elif key == "uniqueid":
|
||||
row[key] = str(row[key])
|
||||
|
||||
def client_price_row(self, products, row):
|
||||
if products and len(products) > 0 and products[0]["priceHistory"]:
|
||||
matched_period = None
|
||||
last_period = None
|
||||
|
||||
for period in products[0]["priceHistory"]:
|
||||
last_period = period
|
||||
start = period['startDate'].date()
|
||||
end = period['endDate'].date() if period['endDate'] else None
|
||||
start_call = row['start_call'].date()
|
||||
|
||||
if end:
|
||||
if start <= start_call <= end:
|
||||
matched_period = period
|
||||
break
|
||||
else:
|
||||
if start_call >= start:
|
||||
matched_period = period
|
||||
break
|
||||
|
||||
f"{(int(row['total_billsec']) / 60):.2f}"
|
||||
if matched_period:
|
||||
row['client_total_cost'] = f"{((int(row['total_billsec']) / 60) * matched_period['price'])}"
|
||||
row["client_price"] = matched_period['price']
|
||||
else:
|
||||
row['client_total_cost'] = f"{((int(row['total_billsec']) / 60) * last_period['price'])}"
|
||||
row["client_price"] = last_period['price']
|
||||
|
||||
|
||||
def _create_excel(self, data: list, hit_report: Optional[bool] = False) -> str:
|
||||
|
||||
if hit_report:
|
||||
header_mapping = {
|
||||
"companyId": "companyId",
|
||||
"uniqueid": "sessionId",
|
||||
"total_billsec": "tempo (billsec)",
|
||||
"custo_HIT": "custo_HIT",
|
||||
"price": "tarifa ($/min)",
|
||||
"client_price": "valor_cobrado"
|
||||
"companyId": "Empresa",
|
||||
"uniqueid": "Identificador da chamada",
|
||||
"src": "Origem",
|
||||
"dst": "Destino",
|
||||
"total_billsec": "Quantidade de segundos",
|
||||
"custo_hit": "Custo HIT",
|
||||
"qtd_token_input": "Quantidade de tokens(input)",
|
||||
"qtd_token_output": "Quantidade de tokens(output)",
|
||||
"client_total_cost": "Custo Cliente",
|
||||
"client_price": "Preço Cliente por Minuto",
|
||||
"start_call": "Inicio",
|
||||
"end_call": "Fim"
|
||||
}
|
||||
else:
|
||||
header_mapping = {
|
||||
|
@ -167,7 +273,8 @@ class TranscriptionReportService:
|
|||
"dst": "Destino",
|
||||
"start_call": "Inicio da Chamada",
|
||||
"total_billsec": "Duração (Em segundos)",
|
||||
"total_min": "Duração (Em minutos)"
|
||||
"total_min": "Duração (Em minutos)",
|
||||
"client_total_cost": "Custo Cliente",
|
||||
}
|
||||
|
||||
# Filtrar e ordenar os dados conforme header_mapping
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
from datetime import datetime
|
||||
|
||||
costs = [
|
||||
{"name": 'test', "date": datetime(2025, 6, 22, 17), "value": 10},
|
||||
{"name": 'test2', "date": datetime(2025, 7, 10, 17), "value": 11},
|
||||
{"name": 'test3', "date": datetime(2025, 8, 10, 17), "value": 12}
|
||||
]
|
||||
|
||||
periods = [
|
||||
{
|
||||
'startDate': datetime(2025, 6, 9, 17, 18, 25, 356000),
|
||||
'endDate': datetime(2025, 7, 10, 17, 18, 25, 356000),
|
||||
'price': 0.05
|
||||
},
|
||||
{
|
||||
'startDate': datetime(2025, 8, 11, 17, 18, 25, 356000),
|
||||
'endDate': None,
|
||||
'price': 0.06
|
||||
}
|
||||
]
|
||||
|
||||
# Verificar se cada cost está dentro de algum período
|
||||
for cost in costs:
|
||||
cost_date = cost['date']
|
||||
cost_value = cost["value"]
|
||||
matched_period = None
|
||||
for period in periods:
|
||||
start = period['startDate']
|
||||
end = period['endDate']
|
||||
if end:
|
||||
if start <= cost_date <= end:
|
||||
matched_period = period
|
||||
break
|
||||
else:
|
||||
if cost_date >= start:
|
||||
matched_period = period
|
||||
break
|
||||
|
||||
if matched_period:
|
||||
print(f"{cost['name']} está dentro da vigência: preço = {matched_period['price']} | value * cost_value: {cost_value * matched_period['price']}")
|
||||
|
|
@ -9,6 +9,7 @@ import { LogOut } from "lucide-react"
|
|||
import TranscriptionTable from "@/components/transcription-table"
|
||||
import ModelPricesTable from "@/components/model-prices-table"
|
||||
import CostUpdateForm from "@/components/cost-update-form"
|
||||
import ProductManagement from "@/components/product-management"
|
||||
|
||||
export default function Dashboard() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
|
@ -61,7 +62,8 @@ export default function Dashboard() {
|
|||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="transcription">Dados de Transcrição</TabsTrigger>
|
||||
<TabsTrigger value="models">Preços dos Modelos</TabsTrigger>
|
||||
<TabsTrigger value="costs">Atualizar Custos</TabsTrigger>
|
||||
{/* <TabsTrigger value="costs">Atualizar Custos</TabsTrigger> */}
|
||||
<TabsTrigger value="products">Produtos</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="transcription">
|
||||
|
@ -91,14 +93,26 @@ export default function Dashboard() {
|
|||
<TabsContent value="costs">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Atualizar Custos Consumidos</CardTitle>
|
||||
<CardDescription>Atualize os custos de uso do produto consumido por período</CardDescription>
|
||||
<CardTitle>Atualizar Custos</CardTitle>
|
||||
<CardDescription>Atualize os custos de uso dos produtos</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CostUpdateForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="products">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Gerenciar Produtos</CardTitle>
|
||||
<CardDescription>Crie, edite e gerencie produtos e seus preços</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ProductManagement />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -24,8 +24,7 @@ interface ModelPrice {
|
|||
billingBy: string
|
||||
billingUnit: number
|
||||
currency: string
|
||||
price: string
|
||||
clientPrice?: string // Campo opcional que pode não vir da API
|
||||
price: string
|
||||
createdAt: {
|
||||
$date: string
|
||||
}
|
||||
|
@ -53,8 +52,7 @@ export default function ModelPricesTable() {
|
|||
type: string
|
||||
billingBy: string
|
||||
billingUnit: string
|
||||
currency: string
|
||||
clientPrice: string
|
||||
currency: string
|
||||
price: string
|
||||
}>({
|
||||
product: "",
|
||||
|
@ -62,14 +60,13 @@ export default function ModelPricesTable() {
|
|||
type: "",
|
||||
billingBy: "",
|
||||
billingUnit: "",
|
||||
currency: "",
|
||||
clientPrice: "",
|
||||
currency: "",
|
||||
price: "",
|
||||
})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// Filtros
|
||||
const [typeFilter, setTypeFilter] = useState("stt")
|
||||
const [typeFilter, setTypeFilter] = useState("")
|
||||
const [providerFilter, setProviderFilter] = useState("")
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
|
@ -123,8 +120,7 @@ export default function ModelPricesTable() {
|
|||
type: item.type,
|
||||
billingBy: item.billingBy,
|
||||
billingUnit: item.billingUnit.toString(),
|
||||
currency: item.currency,
|
||||
clientPrice: item.clientPrice || "",
|
||||
currency: item.currency,
|
||||
price: item.price,
|
||||
})
|
||||
}
|
||||
|
@ -137,8 +133,7 @@ export default function ModelPricesTable() {
|
|||
type: "",
|
||||
billingBy: "",
|
||||
billingUnit: "",
|
||||
currency: "",
|
||||
clientPrice: "",
|
||||
currency: "",
|
||||
price: "",
|
||||
})
|
||||
}
|
||||
|
@ -158,8 +153,7 @@ export default function ModelPricesTable() {
|
|||
type: editValues.type,
|
||||
billingBy: editValues.billingBy,
|
||||
billingUnit: Number.parseInt(editValues.billingUnit),
|
||||
currency: editValues.currency,
|
||||
clientPrice: editValues.clientPrice,
|
||||
currency: editValues.currency,
|
||||
price: editValues.price,
|
||||
}),
|
||||
})
|
||||
|
@ -173,8 +167,7 @@ export default function ModelPricesTable() {
|
|||
type: "",
|
||||
billingBy: "",
|
||||
billingUnit: "",
|
||||
currency: "",
|
||||
clientPrice: "",
|
||||
currency: "",
|
||||
price: "",
|
||||
})
|
||||
} else {
|
||||
|
@ -194,7 +187,7 @@ export default function ModelPricesTable() {
|
|||
}, [])
|
||||
|
||||
// Opções para os selects
|
||||
const typeOptions = ["stt", "tts", "llm", "embedding", "vision"]
|
||||
const typeOptions = ["stt", "tts", "llm", "embedding", "input", "output", "vision"]
|
||||
const providerOptions = ["openai", "aws", "google", "anthropic", "mistral", "azure"]
|
||||
const currencyOptions = ["dollar", "real", "euro"]
|
||||
const billingByOptions = ["second", "minute", "character", "token", "image", "request"]
|
||||
|
@ -224,7 +217,7 @@ export default function ModelPricesTable() {
|
|||
id="providerFilter"
|
||||
value={providerFilter}
|
||||
onChange={(e) => setProviderFilter(e.target.value)}
|
||||
placeholder="openai,aws"
|
||||
placeholder="openai,anthropic"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -263,8 +256,7 @@ export default function ModelPricesTable() {
|
|||
<TableHead>Cobrança Por</TableHead>
|
||||
<TableHead>Unidade</TableHead>
|
||||
<TableHead>Moeda</TableHead>
|
||||
<TableHead>Preço</TableHead>
|
||||
<TableHead>Preço Cliente</TableHead>
|
||||
<TableHead>Preço</TableHead>
|
||||
<TableHead>Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
@ -395,22 +387,7 @@ export default function ModelPricesTable() {
|
|||
`$${item.price}`
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
value={editValues.clientPrice}
|
||||
onChange={(e) => setEditValues((prev) => ({ ...prev, clientPrice: e.target.value }))}
|
||||
className="w-24"
|
||||
placeholder="0.000"
|
||||
/>
|
||||
) : item.clientPrice ? (
|
||||
`$${item.clientPrice}`
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<div className="flex gap-2">
|
||||
|
|
|
@ -0,0 +1,543 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Edit2, Plus, Save, X, Loader2, RefreshCw, History } from "lucide-react"
|
||||
|
||||
// Usar as variáveis de ambiente para as URLs
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1"
|
||||
|
||||
interface PriceHistory {
|
||||
startDate: string
|
||||
endDate: string | null
|
||||
price: number
|
||||
}
|
||||
|
||||
interface Product {
|
||||
_id: string
|
||||
name: string
|
||||
description: string
|
||||
priceHistory: PriceHistory[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
__v: number
|
||||
}
|
||||
|
||||
interface ProductsResponse {
|
||||
msg: string
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
interface CreateProductRequest {
|
||||
name: string
|
||||
description: string
|
||||
price: number
|
||||
}
|
||||
|
||||
interface UpdateProductRequest {
|
||||
name?: string
|
||||
description?: string
|
||||
price?: number
|
||||
}
|
||||
|
||||
export default function ProductManagement() {
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState("")
|
||||
|
||||
// Estados para criação de produto
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [createForm, setCreateForm] = useState<CreateProductRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
price: 0,
|
||||
})
|
||||
|
||||
// Estados para edição de produto
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [editForm, setEditForm] = useState<UpdateProductRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
price: 0,
|
||||
})
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
|
||||
// Estados para histórico de preços
|
||||
const [historyDialogOpen, setHistoryDialogOpen] = useState(false)
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem("access_token")
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
const fetchProducts = async () => {
|
||||
setIsLoading(true)
|
||||
setError("")
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/billing/products`, {
|
||||
method: "GET",
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result: ProductsResponse = await response.json()
|
||||
setProducts(result.products || [])
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
// Para erro 400, exibir a mensagem específica da API
|
||||
if (response.status === 400 && errorData.msg) {
|
||||
setError(errorData.msg)
|
||||
} else {
|
||||
setError(errorData.message || errorData.msg || "Erro ao buscar produtos")
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro de conexão com o servidor")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const createProduct = async () => {
|
||||
if (!createForm.name || !createForm.description || createForm.price <= 0) {
|
||||
setError("Preencha todos os campos corretamente")
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreating(true)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/billing/product`, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(createForm),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
setSuccess("Produto criado com sucesso!")
|
||||
setCreateForm({ name: "", description: "", price: 0 })
|
||||
setIsCreateDialogOpen(false)
|
||||
await fetchProducts()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
// Para erro 400, exibir a mensagem específica da API
|
||||
if (response.status === 400 && errorData.msg) {
|
||||
setError(errorData.msg)
|
||||
} else {
|
||||
setError(errorData.message || errorData.msg || "Erro ao criar produto")
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro de conexão com o servidor")
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateProduct = async (productId: string) => {
|
||||
// Verificar se há pelo menos um campo para atualizar
|
||||
if (Object.keys(editForm).length === 0) {
|
||||
setError("Nenhuma alteração para salvar")
|
||||
return
|
||||
}
|
||||
|
||||
// Se o preço estiver definido, verificar se é válido
|
||||
if (editForm.price !== undefined && (isNaN(editForm.price) || editForm.price <= 0)) {
|
||||
setError("Digite um preço válido")
|
||||
return
|
||||
}
|
||||
|
||||
setIsUpdating(true)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/billing/product/${productId}`, {
|
||||
method: "PATCH",
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(editForm),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
setSuccess("Produto atualizado com sucesso!")
|
||||
setEditingId(null)
|
||||
setEditForm({})
|
||||
await fetchProducts()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
// Para erro 400, exibir a mensagem específica da API
|
||||
if (response.status === 400 && errorData.msg) {
|
||||
setError(errorData.msg)
|
||||
} else {
|
||||
setError(errorData.message || errorData.msg || "Erro ao atualizar produto")
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro de conexão com o servidor")
|
||||
} finally {
|
||||
setIsUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const startEdit = (product: Product) => {
|
||||
setEditingId(product._id)
|
||||
const currentPrice = getCurrentPrice(product)
|
||||
setEditForm({
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
price: currentPrice,
|
||||
})
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null)
|
||||
setEditForm({})
|
||||
}
|
||||
|
||||
const getCurrentPrice = (product: Product): number => {
|
||||
return product.priceHistory.find((p) => p.endDate === null)?.price || 0
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
return new Date(dateString).toLocaleString("pt-BR")
|
||||
}
|
||||
|
||||
const formatDateCompact = (dateString: string): string => {
|
||||
const date = new Date(dateString)
|
||||
return `${date.getDate().toString().padStart(2, "0")}/${(date.getMonth() + 1).toString().padStart(2, "0")}/${date.getFullYear()}, ${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const showHistory = (product: Product) => {
|
||||
setSelectedProduct(product)
|
||||
setHistoryDialogOpen(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Cabeçalho com botão de criar */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Produtos Cadastrados</h3>
|
||||
<p className="text-sm text-gray-500">Gerencie produtos e seus preços</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={fetchProducts} disabled={isLoading} variant="outline">
|
||||
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
|
||||
Atualizar
|
||||
</Button>
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Novo Produto
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Criar Novo Produto</DialogTitle>
|
||||
<DialogDescription>Preencha as informações do novo produto</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Nome do Produto *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={createForm.name}
|
||||
onChange={(e) => setCreateForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Ex: Produto HIT"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Descrição *</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={createForm.description}
|
||||
onChange={(e) => setCreateForm((prev) => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Descreva o produto..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="price">Preço Inicial *</Label>
|
||||
<Input
|
||||
id="price"
|
||||
type="number"
|
||||
step="0.001"
|
||||
value={createForm.price}
|
||||
onChange={(e) =>
|
||||
setCreateForm((prev) => ({ ...prev, price: Number.parseFloat(e.target.value) || 0 }))
|
||||
}
|
||||
placeholder="0.060"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button onClick={createProduct} disabled={isCreating} className="flex-1">
|
||||
{isCreating ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
|
||||
Criar Produto
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)} disabled={isCreating}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert>
|
||||
<AlertDescription>{success}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Tabela de Produtos */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lista de Produtos ({products.length} produtos)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{products.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead>Descrição</TableHead>
|
||||
<TableHead>Preço Atual</TableHead>
|
||||
<TableHead>Criado em</TableHead>
|
||||
<TableHead>Atualizado em</TableHead>
|
||||
<TableHead>Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{products.map((product) => (
|
||||
<TableRow key={product._id}>
|
||||
<TableCell>
|
||||
{editingId === product._id ? (
|
||||
<Input
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
) : (
|
||||
<div className="font-medium">{product.name}</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editingId === product._id ? (
|
||||
<Textarea
|
||||
value={editForm.description}
|
||||
onChange={(e) => setEditForm((prev) => ({ ...prev, description: e.target.value }))}
|
||||
rows={2}
|
||||
/>
|
||||
) : (
|
||||
<div className="max-w-xs truncate" title={product.description}>
|
||||
{product.description}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editingId === product._id ? (
|
||||
<Input
|
||||
type="number"
|
||||
step="0.001"
|
||||
value={editForm.price}
|
||||
onChange={(e) =>
|
||||
setEditForm((prev) => ({ ...prev, price: Number.parseFloat(e.target.value) || 0 }))
|
||||
}
|
||||
className="w-24"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="cursor-pointer hover:bg-gray-200 transition-colors"
|
||||
onClick={() => showHistory(product)}
|
||||
title="Clique para ver o histórico de preços"
|
||||
>
|
||||
${getCurrentPrice(product)}
|
||||
</Badge>
|
||||
{product.priceHistory && product.priceHistory.length > 1 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{product.priceHistory.length} alterações
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(product.createdAt)}</TableCell>
|
||||
<TableCell>{formatDate(product.updatedAt)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
{editingId === product._id ? (
|
||||
<>
|
||||
<Button size="sm" onClick={() => updateProduct(product._id)} disabled={isUpdating}>
|
||||
{isUpdating ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={cancelEdit} disabled={isUpdating}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={() => startEdit(product)}>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => showHistory(product)}>
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
{isLoading ? "Carregando..." : "Nenhum produto encontrado. Crie seu primeiro produto!"}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dialog de Histórico de Preços - Ajustado para caber na tela */}
|
||||
<Dialog open={historyDialogOpen} onOpenChange={setHistoryDialogOpen}>
|
||||
<DialogContent className="w-[95%] max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Histórico de Preços - {selectedProduct?.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Visualize todas as alterações de preço deste produto ({selectedProduct?.priceHistory?.length || 0}{" "}
|
||||
registros)
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedProduct && selectedProduct.priceHistory && selectedProduct.priceHistory.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Data Início</TableHead>
|
||||
<TableHead>Data Fim</TableHead>
|
||||
<TableHead>Preço</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Duração</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{selectedProduct.priceHistory
|
||||
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
|
||||
.map((history, index) => {
|
||||
const startDate = new Date(history.startDate)
|
||||
const endDate = history.endDate ? new Date(history.endDate) : null
|
||||
const duration = endDate
|
||||
? Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
: Math.ceil((new Date().getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
return (
|
||||
<TableRow key={index} className={history.endDate === null ? "bg-blue-50" : ""}>
|
||||
<TableCell className="text-sm">{formatDateCompact(history.startDate)}</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{history.endDate ? formatDateCompact(history.endDate) : "Em vigor"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={history.endDate === null ? "default" : "secondary"}>
|
||||
${history.price}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={history.endDate === null ? "default" : "outline"}>
|
||||
{history.endDate === null ? "Atual" : "Histórico"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">
|
||||
{duration} {duration === 1 ? "dia" : "dias"}
|
||||
{history.endDate === null && " (em vigor)"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Resumo do histórico - Simplificado para economizar espaço */}
|
||||
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||
<h4 className="font-medium text-sm text-gray-700 mb-2">Resumo do Histórico</h4>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Total de alterações:</span>
|
||||
<div className="font-medium">{selectedProduct.priceHistory.length}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Preço inicial:</span>
|
||||
<div className="font-medium">
|
||||
$
|
||||
{selectedProduct.priceHistory
|
||||
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime())[0]
|
||||
?.price.toFixed(3)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Preço atual:</span>
|
||||
<div className="font-medium">${getCurrentPrice(selectedProduct)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Produto criado em:</span>
|
||||
<div className="font-medium">{formatDateCompact(selectedProduct.createdAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Nenhum histórico de preços encontrado para este produto.
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ThemeProvider as NextThemesProvider,
|
||||
type ThemeProviderProps,
|
||||
} from 'next-themes'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
|
@ -22,19 +22,22 @@ interface ClientTranscriptionData {
|
|||
start_call: string
|
||||
total_billsec: number
|
||||
total_min: string
|
||||
client_total_cost: string
|
||||
}
|
||||
|
||||
interface HitTranscriptionData {
|
||||
uniqueid: string
|
||||
src: string
|
||||
dst: string
|
||||
start_call: string
|
||||
end_call: string
|
||||
total_billsec: number
|
||||
companyId: string
|
||||
custo_HIT: number
|
||||
price: number
|
||||
client_price: string
|
||||
companyId: string // Empresa
|
||||
uniqueid: string // Identificador da chamada
|
||||
src: string // Origem
|
||||
dst: string // Destino
|
||||
total_billsec: number // Quantidade de segundos
|
||||
qtd_token_input: number // Quantidade de tokens(input)
|
||||
qtd_token_output: number // Quantidade de tokens(output)
|
||||
custo_hit: string // Custo HIT
|
||||
client_total_cost: string // Custo Cliente
|
||||
client_price: string // Preço Cliente por Minuto
|
||||
start_call: string // Inicio
|
||||
end_call: string // Fim
|
||||
}
|
||||
|
||||
type TranscriptionData = ClientTranscriptionData | HitTranscriptionData
|
||||
|
@ -239,7 +242,7 @@ export default function TranscriptionTable() {
|
|||
<Label htmlFor="pageSize">Registros por página</Label>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
onValueChange={(value:any) => {
|
||||
setPageSize(Number(value))
|
||||
setCurrentPage(1)
|
||||
}}
|
||||
|
@ -299,53 +302,65 @@ export default function TranscriptionTable() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID Único</TableHead>
|
||||
<TableHead>Origem</TableHead>
|
||||
<TableHead>Destino</TableHead>
|
||||
<TableHead>Início</TableHead>
|
||||
{who === "hit" && <TableHead>Fim</TableHead>}
|
||||
<TableHead>Duração (s)</TableHead>
|
||||
{who === "client" ? (
|
||||
<TableHead>Total (min)</TableHead>
|
||||
) : (
|
||||
{who === "hit" ? (
|
||||
<>
|
||||
<TableHead>Empresa</TableHead>
|
||||
<TableHead>Identificador da chamada</TableHead>
|
||||
<TableHead>Origem</TableHead>
|
||||
<TableHead>Destino</TableHead>
|
||||
<TableHead>Quantidade de segundos</TableHead>
|
||||
<TableHead>Quantidade de tokens (input)</TableHead>
|
||||
<TableHead>Quantidade de tokens (output)</TableHead>
|
||||
<TableHead>Custo HIT</TableHead>
|
||||
<TableHead>Preço</TableHead>
|
||||
<TableHead>Preço Cliente</TableHead>
|
||||
<TableHead>Custo Cliente</TableHead>
|
||||
<TableHead>Preço Cliente por Minuto</TableHead>
|
||||
<TableHead>Início</TableHead>
|
||||
<TableHead>Fim</TableHead>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableHead>ID Único</TableHead>
|
||||
<TableHead>Origem</TableHead>
|
||||
<TableHead>Destino</TableHead>
|
||||
<TableHead>Início</TableHead>
|
||||
<TableHead>Duração (s)</TableHead>
|
||||
<TableHead>Total (min)</TableHead>
|
||||
<TableHead>Custo Cliente</TableHead>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<TableRow key={item.uniqueid || index}>
|
||||
<TableCell>{item.uniqueid || "-"}</TableCell>
|
||||
<TableCell>{item.src || "-"}</TableCell>
|
||||
<TableCell>{item.dst || "-"}</TableCell>
|
||||
<TableCell>{item.start_call || "-"}</TableCell>
|
||||
{who === "hit" && <TableCell>{(item as HitTranscriptionData).end_call || "-"}</TableCell>}
|
||||
<TableCell>{item.total_billsec || "-"}</TableCell>
|
||||
{who === "client" ? (
|
||||
<TableCell>{(item as ClientTranscriptionData).total_min || "-"}</TableCell>
|
||||
) : (
|
||||
<TableRow key={(item as any).uniqueid || index}>
|
||||
{who === "hit" ? (
|
||||
<>
|
||||
<TableCell>{(item as HitTranscriptionData).companyId || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).uniqueid || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).src || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).dst || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).total_billsec || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).qtd_token_input || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).qtd_token_output || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{(item as HitTranscriptionData).custo_HIT
|
||||
? `$${(item as HitTranscriptionData).custo_HIT.toFixed(6)}`
|
||||
: "-"}
|
||||
{(item as HitTranscriptionData).custo_hit ? `$${(item as HitTranscriptionData).custo_hit}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).client_total_cost || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{(item as HitTranscriptionData).price
|
||||
? `$${(item as HitTranscriptionData).price.toFixed(4)}`
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{(item as HitTranscriptionData).client_price
|
||||
? `$${(item as HitTranscriptionData).client_price}`
|
||||
: "-"}
|
||||
{(item as HitTranscriptionData).client_price ? `$${(item as HitTranscriptionData).client_price}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).start_call || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).end_call || "-"}</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell>{(item as ClientTranscriptionData).uniqueid || "-"}</TableCell>
|
||||
<TableCell>{(item as ClientTranscriptionData).src || "-"}</TableCell>
|
||||
<TableCell>{(item as ClientTranscriptionData).dst || "-"}</TableCell>
|
||||
<TableCell>{(item as ClientTranscriptionData).start_call || "-"}</TableCell>
|
||||
<TableCell>{(item as ClientTranscriptionData).total_billsec || "-"}</TableCell>
|
||||
<TableCell>{(item as ClientTranscriptionData).total_min || "-"}</TableCell>
|
||||
<TableCell>{(item as ClientTranscriptionData).client_total_cost || "-"}</TableCell>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
|
@ -0,0 +1,24 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
|
@ -0,0 +1,185 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@ -0,0 +1,18 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "transcriptio-usage-frontend",
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -9,17 +9,18 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.513.0",
|
||||
"lucide-react": "^0.514.0",
|
||||
"next": "15.2.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
|
Loading…
Reference in New Issue