From f9786de1b786c469f355f8a1489d412bb36a21f2 Mon Sep 17 00:00:00 2001 From: adriano Date: Thu, 12 Jun 2025 17:58:22 -0300 Subject: [PATCH] feat: added tab products to manager the client price --- backend/Pipfile | 1 + backend/Pipfile.lock | 255 ++++++--- backend/app/__init__.py | 2 + backend/app/config.py | 3 + backend/app/docs/biling_models.py | 15 + backend/app/routes/billing_routes.py | 41 ++ backend/app/services/report_service.py | 217 ++++++-- backend/test.py | 41 ++ frontend/app/dashboard/page.tsx | 20 +- frontend/components/model-prices-table.tsx | 47 +- frontend/components/product-management.tsx | 543 +++++++++++++++++++ frontend/components/theme-provider.tsx | 11 + frontend/components/transcription-table.tsx | 101 ++-- frontend/components/ui/dialog.tsx | 97 ++++ frontend/components/ui_old/alert.tsx | 66 +++ frontend/components/ui_old/badge.tsx | 46 ++ frontend/components/ui_old/button.tsx | 59 ++ frontend/components/ui_old/card.tsx | 92 ++++ frontend/components/ui_old/input.tsx | 21 + frontend/components/ui_old/label.tsx | 24 + frontend/components/ui_old/select.tsx | 185 +++++++ frontend/components/ui_old/table.tsx | 116 ++++ frontend/components/ui_old/tabs.tsx | 66 +++ frontend/components/ui_old/textarea.tsx | 18 + frontend/package-lock.json | 564 +++++++++++--------- frontend/package.json | 7 +- 26 files changed, 2213 insertions(+), 445 deletions(-) create mode 100644 backend/app/docs/biling_models.py create mode 100644 backend/app/routes/billing_routes.py create mode 100644 backend/test.py create mode 100644 frontend/components/product-management.tsx create mode 100644 frontend/components/theme-provider.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui_old/alert.tsx create mode 100644 frontend/components/ui_old/badge.tsx create mode 100644 frontend/components/ui_old/button.tsx create mode 100644 frontend/components/ui_old/card.tsx create mode 100644 frontend/components/ui_old/input.tsx create mode 100644 frontend/components/ui_old/label.tsx create mode 100644 frontend/components/ui_old/select.tsx create mode 100644 frontend/components/ui_old/table.tsx create mode 100644 frontend/components/ui_old/tabs.tsx create mode 100644 frontend/components/ui_old/textarea.tsx diff --git a/backend/Pipfile b/backend/Pipfile index 0c3d417..0fc95a2 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -19,6 +19,7 @@ flask-jwt-extended = "*" flask-bcrypt = "*" flask-cors = "*" mypy = "*" +requests = "*" [dev-packages] diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 0d0d396..4f284a6 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -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", diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 161b3c0..cc31707 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -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) diff --git a/backend/app/config.py b/backend/app/config.py index f921921..9e24aab 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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") + diff --git a/backend/app/docs/biling_models.py b/backend/app/docs/biling_models.py new file mode 100644 index 0000000..f7e4c9b --- /dev/null +++ b/backend/app/docs/biling_models.py @@ -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'), +}) \ No newline at end of file diff --git a/backend/app/routes/billing_routes.py b/backend/app/routes/billing_routes.py new file mode 100644 index 0000000..2be2713 --- /dev/null +++ b/backend/app/routes/billing_routes.py @@ -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/') +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 \ No newline at end of file diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index 3044749..2d584a0 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -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 diff --git a/backend/test.py b/backend/test.py new file mode 100644 index 0000000..eac0133 --- /dev/null +++ b/backend/test.py @@ -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']}") + diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 850e3a5..11dc478 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -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() { Dados de Transcrição Preços dos Modelos - Atualizar Custos + {/* Atualizar Custos */} + Produtos @@ -91,14 +93,26 @@ export default function Dashboard() { - Atualizar Custos Consumidos - Atualize os custos de uso do produto consumido por período + Atualizar Custos + Atualize os custos de uso dos produtos + + + + + Gerenciar Produtos + Crie, edite e gerencie produtos e seus preços + + + + + + diff --git a/frontend/components/model-prices-table.tsx b/frontend/components/model-prices-table.tsx index a66f889..b9bd3d3 100644 --- a/frontend/components/model-prices-table.tsx +++ b/frontend/components/model-prices-table.tsx @@ -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" /> @@ -263,8 +256,7 @@ export default function ModelPricesTable() { Cobrança Por Unidade Moeda - Preço - Preço Cliente + Preço Ações @@ -395,22 +387,7 @@ export default function ModelPricesTable() { `$${item.price}` )} - - {isEditing ? ( - setEditValues((prev) => ({ ...prev, clientPrice: e.target.value }))} - className="w-24" - placeholder="0.000" - /> - ) : item.clientPrice ? ( - `$${item.clientPrice}` - ) : ( - "-" - )} - + {isEditing ? (
diff --git a/frontend/components/product-management.tsx b/frontend/components/product-management.tsx new file mode 100644 index 0000000..8288649 --- /dev/null +++ b/frontend/components/product-management.tsx @@ -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([]) + 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({ + name: "", + description: "", + price: 0, + }) + + // Estados para edição de produto + const [editingId, setEditingId] = useState(null) + const [editForm, setEditForm] = useState({ + 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(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 ( +
+ {/* Cabeçalho com botão de criar */} +
+
+

Produtos Cadastrados

+

Gerencie produtos e seus preços

+
+
+ + + + + + + + Criar Novo Produto + Preencha as informações do novo produto + +
+
+ + setCreateForm((prev) => ({ ...prev, name: e.target.value }))} + placeholder="Ex: Produto HIT" + /> +
+
+ +