we are so back

This commit is contained in:
zman 2025-05-05 14:05:12 -04:00
parent 11aa4cda16
commit 5c85411c69
20 changed files with 2417 additions and 33 deletions

View File

@ -0,0 +1,94 @@
Name,Set code,Set name,Collector number,Foil,Rarity,Quantity,ManaBox ID,Scryfall ID,Purchase price,Misprint,Altered,Condition,Language,Purchase price currency
Undergrowth Leopard,TDM,Tarkir: Dragonstorm,165,foil,common,1,104307,67ab8f9a-b17c-452f-b4ef-a3f91909e3de,0.08,false,false,near_mint,en,USD
Gurmag Nightwatch,TDM,Tarkir: Dragonstorm,190,foil,common,1,104369,de731430-6bbf-4782-953e-b69c46353959,0.03,false,false,near_mint,en,USD
Mystic Monastery,TDM,Tarkir: Dragonstorm,262,foil,uncommon,1,104945,c7b8a01c-c400-47c7-8270-78902efe850e,0.21,false,false,near_mint,en,USD
Stormshriek Feral // Flush Out,TDM,Tarkir: Dragonstorm,124,foil,common,1,104447,0ec92c44-7cf0-48a5-a3ca-bc633496d887,0.1,false,false,near_mint,en,USD
Reigning Victor,TDM,Tarkir: Dragonstorm,216,foil,common,1,104334,a394112a-032b-4047-887a-6522cf7b83d5,0.02,false,false,near_mint,en,USD
Dragonbroods' Relic,TDM,Tarkir: Dragonstorm,140,foil,uncommon,1,104569,3d634087-77ba-4543-aa7a-8a3774d69cd7,0.13,false,false,near_mint,en,USD
Sagu Wildling // Roost Seek,TDM,Tarkir: Dragonstorm,306,foil,common,1,104903,b72ee8f9-5e79-4f77-ae7e-e4c274f78187,0.13,false,false,near_mint,en,USD
Sibsig Appraiser,TDM,Tarkir: Dragonstorm,56,foil,common,1,105135,670c5b96-bac6-449b-a2bd-cb43750d3911,0.05,false,false,near_mint,en,USD
Sage of the Fang,TDM,Tarkir: Dragonstorm,155,foil,uncommon,1,105123,1ebf4a9d-d90c-4017-9f00-fca89899f301,0.09,false,false,near_mint,en,USD
Snowmelt Stag,TDM,Tarkir: Dragonstorm,57,foil,common,1,104869,a6b3b131-704a-4586-84f8-db465cd4a277,0.04,false,false,near_mint,en,USD
Tranquil Cove,TDM,Tarkir: Dragonstorm,270,foil,common,1,104249,1c4efa6c-4f29-41cd-a728-bf0e479ace05,0.07,false,false,near_mint,en,USD
Rally the Monastery,TDM,Tarkir: Dragonstorm,19,foil,uncommon,1,104136,b56e0037-8143-4c13-83e1-0c3f44e685ea,0.22,false,false,near_mint,en,USD
Dragon's Prey,TDM,Tarkir: Dragonstorm,79,foil,common,1,104754,7a6004ff-4180-4332-8b51-960f8c7521d9,0.03,false,false,near_mint,en,USD
Ambling Stormshell,TDM,Tarkir: Dragonstorm,37,foil,rare,1,104942,c74d4a57-0f66-4965-9ed7-f88a08aa1d15,0.45,false,false,near_mint,en,USD
Mountain,TDM,Tarkir: Dragonstorm,275,foil,common,1,104397,fe0865ba-47c0-40bc-b0c6-e1ea5ae08a98,1.83,false,false,near_mint,en,USD
Mardu Devotee,TDM,Tarkir: Dragonstorm,16,foil,common,1,104366,da45e9b0-a4f6-413b-9e62-666c511eb5b0,0.09,false,false,near_mint,en,USD
Swiftwater Cliffs,TDM,Tarkir: Dragonstorm,268,foil,common,1,104361,ca53fb19-b8ca-485b-af1a-5117ae54bfe3,0.11,false,false,near_mint,en,USD
Adorned Crocodile,TDM,Tarkir: Dragonstorm,69,foil,common,1,105159,bb13a34b-6ac8-47cb-9e91-47106a585fc1,0.05,false,false,near_mint,en,USD
Dusyut Earthcarver,TDM,Tarkir: Dragonstorm,141,foil,common,1,104352,b98ecc96-f557-479a-8685-2b5487d5b407,0.02,false,false,near_mint,en,USD
Knockout Maneuver,TDM,Tarkir: Dragonstorm,147,foil,uncommon,1,105149,9d218831-2a41-46a3-8e9d-93462cae5cab,0.07,false,false,near_mint,en,USD
Roiling Dragonstorm,TDM,Tarkir: Dragonstorm,55,foil,uncommon,1,104280,455f4c96-684b-4b14-bd21-6799da2e1fa7,0.22,false,false,near_mint,en,USD
Dragonclaw Strike,TDM,Tarkir: Dragonstorm,180,foil,uncommon,1,105161,bc7692ef-7091-4365-85a8-1edbd374f279,0.12,false,false,near_mint,en,USD
Seize Opportunity,TDM,Tarkir: Dragonstorm,119,foil,common,1,104391,f7818d28-b9a5-4341-9adc-666070b8878d,0.03,false,false,near_mint,en,USD
Shock Brigade,TDM,Tarkir: Dragonstorm,120,foil,common,1,104700,66940466-8e9d-4a85-bfb0-e92189b7a121,0.11,false,false,near_mint,en,USD
Hardened Tactician,TDM,Tarkir: Dragonstorm,191,foil,uncommon,1,104780,86b225cb-5c45-4da1-a64e-b04091e483e8,0.43,false,false,near_mint,en,USD
Stormplain Detainment,TDM,Tarkir: Dragonstorm,28,foil,common,1,104135,39f3aab5-7b54-4b55-8114-c6f9f79c255d,0.04,false,false,near_mint,en,USD
Formation Breaker,TDM,Tarkir: Dragonstorm,143,foil,uncommon,1,105136,67ab8e8f-3ef6-4339-8c66-68c5aca4867a,0.08,false,false,near_mint,en,USD
Duty Beyond Death,TDM,Tarkir: Dragonstorm,10,foil,uncommon,1,104265,2e92640d-768b-4357-905f-bea017d351cc,1.11,false,false,near_mint,en,USD
Piercing Exhale,TDM,Tarkir: Dragonstorm,151,foil,common,1,104891,b2a0deb9-5bc3-42d5-9e1e-5f463d176aef,0.04,false,false,near_mint,en,USD
Trade Route Envoy,TDM,Tarkir: Dragonstorm,163,foil,common,1,105174,f0c89d95-d697-4cfa-9dfa-52d7adb96176,0.05,false,false,near_mint,en,USD
Thornwood Falls,TDM,Tarkir: Dragonstorm,269,foil,common,1,104376,ebb502c2-5fd0-46a9-b77d-010f4a942056,0.07,false,false,near_mint,en,USD
Kin-Tree Nurturer,TDM,Tarkir: Dragonstorm,83,foil,common,1,105124,2177ef64-28bf-4acf-b1f1-c1408f03c411,0.03,false,false,near_mint,en,USD
Rebellious Strike,TDM,Tarkir: Dragonstorm,20,foil,common,1,104949,c9bafe19-3bd6-4da0-b3e5-e0b89262504c,0.06,false,false,near_mint,en,USD
Scoured Barrens,TDM,Tarkir: Dragonstorm,267,foil,common,1,104346,b4b47b80-69ed-44b0-afa0-ca90206dc16d,0.06,false,false,near_mint,en,USD
Avenger of the Fallen,TDM,Tarkir: Dragonstorm,73,foil,rare,1,104984,d5397366-151f-46b0-b9b2-fa4d5bd892d8,0.68,false,false,near_mint,en,USD
Dismal Backwater,TDM,Tarkir: Dragonstorm,254,foil,common,1,104238,082b52c9-c46e-44d3-b723-546ba528e07b,0.07,false,false,near_mint,en,USD
Rediscover the Way,TDM,Tarkir: Dragonstorm,215,normal,rare,1,104313,79d6decf-afd5-4e96-b87e-fd7ab7e3c068,0.19,false,false,near_mint,en,USD
Auroral Procession,TDM,Tarkir: Dragonstorm,169,normal,uncommon,1,104701,672f94ad-65d6-4c7d-925d-165ef264626f,0.22,false,false,near_mint,en,USD
Runescale Stormbrood // Chilling Screech,TDM,Tarkir: Dragonstorm,316,normal,uncommon,1,104733,72e8f916-5a01-4918-bcb5-7fd69fe32785,0.31,false,false,near_mint,en,USD
Stadium Headliner,TDM,Tarkir: Dragonstorm,122,normal,rare,1,104552,37d4ab2a-a06a-4768-b5e1-e1def957d7f4,0.44,false,false,near_mint,en,USD
Nature's Rhythm,TDM,Tarkir: Dragonstorm,150,normal,rare,1,104460,1397d904-c51d-451e-8505-7f3118acc1f6,3.08,false,false,near_mint,en,USD
Karakyk Guardian,TDM,Tarkir: Dragonstorm,198,normal,uncommon,1,104859,a4c77b08-c3f6-4458-8636-f226f9843b6d,0.08,false,false,near_mint,en,USD
"Anafenza, Unyielding Lineage",TDM,Tarkir: Dragonstorm,2,normal,rare,1,104258,29957f49-9a6b-42f6-b2fb-b48f653ab725,0.22,false,false,near_mint,en,USD
"Narset, Jeskai Waymaster",TDM,Tarkir: Dragonstorm,209,normal,rare,1,103995,6b77cbc1-dbc8-44d9-aa29-15cbb19afecd,0.22,false,false,near_mint,en,USD
Stillness in Motion,TDM,Tarkir: Dragonstorm,59,normal,rare,1,104864,a6289251-17e4-4987-96b9-2fb1a8f90e2a,0.17,false,false,near_mint,en,USD
Thunder of Unity,TDM,Tarkir: Dragonstorm,231,normal,rare,1,104671,5c953b36-f5e4-4258-91cb-f07e799321f7,0.14,false,false,near_mint,en,USD
The Sibsig Ceremony,TDM,Tarkir: Dragonstorm,340,normal,rare,1,104719,6daa156c-478f-47dd-9284-b95e82ccfd68,0.67,false,false,near_mint,en,USD
Tersa Lightshatter,TDM,Tarkir: Dragonstorm,127,normal,rare,1,104825,99e96b34-b1c4-4647-a38e-2cf1aedaaace,2.31,false,false,near_mint,en,USD
Forest,TDM,Tarkir: Dragonstorm,286,normal,common,1,104324,8e3e83d2-96ba-4d5c-a1ed-6c08a90b339c,0.07,false,false,near_mint,en,USD
Windcrag Siege,TDM,Tarkir: Dragonstorm,235,normal,rare,1,104534,31a8329b-23a1-4c49-a579-a5da8d01435a,1.68,false,false,near_mint,en,USD
Mountain,TDM,Tarkir: Dragonstorm,284,normal,common,1,104274,3df7c206-97b6-49d7-ba01-7a35fd8c61d9,0.05,false,false,near_mint,en,USD
Inevitable Defeat,TDM,Tarkir: Dragonstorm,194,normal,rare,1,103997,9d677980-b608-407e-9f17-790a81263f15,0.28,false,false,near_mint,en,USD
Dalkovan Encampment,TDM,Tarkir: Dragonstorm,394,normal,rare,1,104670,5af006f6-135e-4ea0-8ce4-7824934e87da,0.72,false,false,near_mint,en,USD
Host of the Hereafter,TDM,Tarkir: Dragonstorm,193,normal,uncommon,1,104448,0f182957-8133-45a7-80a3-1944bead4d43,0.14,false,false,near_mint,en,USD
"Sarkhan, Dragon Ascendant",TDM,Tarkir: Dragonstorm,118,normal,rare,1,104003,c2200646-7b7c-489d-bbae-16b03e1d7fb2,0.32,false,false,near_mint,en,USD
Stormscale Scion,TDM,Tarkir: Dragonstorm,123,normal,mythic,1,103987,0ac43386-bd32-425c-8776-cec00b064cbc,6.78,false,false,near_mint,en,USD
Dragon Sniper,TDM,Tarkir: Dragonstorm,139,normal,uncommon,1,105120,074b1e00-45bb-4436-8f5e-058512b2d08a,0.25,false,false,near_mint,en,USD
Island,TDM,Tarkir: Dragonstorm,273,normal,common,1,104276,4208e66c-8c98-4c48-ab07-8523c0b26ca4,1.02,false,false,near_mint,en,USD
Yathan Roadwatcher,TDM,Tarkir: Dragonstorm,236,normal,rare,1,104800,8e77339b-dd82-481c-9ee2-4156ca69ad35,0.14,false,false,near_mint,en,USD
Nomad Outpost,TDM,Tarkir: Dragonstorm,263,normal,uncommon,1,104868,a68fbeaa-941f-4d53-becd-f93ed22b9a54,0.12,false,false,near_mint,en,USD
Sage of the Skies,TDM,Tarkir: Dragonstorm,22,normal,rare,1,104710,6ade6918-6d1d-448d-ab56-93996051e9a9,0.21,false,false,near_mint,en,USD
Kheru Goldkeeper,TDM,Tarkir: Dragonstorm,199,normal,uncommon,1,104798,8d11183a-57f5-4ddb-8a6e-15fff704b114,0.18,false,false,near_mint,en,USD
All-Out Assault,TDM,Tarkir: Dragonstorm,167,normal,mythic,1,104348,b74876d8-f6a6-4b47-b960-b01a331bab01,4.11,false,false,near_mint,en,USD
Winternight Stories,TDM,Tarkir: Dragonstorm,67,normal,rare,1,104693,64d9367c-f50c-4568-aa63-6760c44ecaeb,0.44,false,false,near_mint,en,USD
New Way Forward,TDM,Tarkir: Dragonstorm,211,normal,rare,1,104996,d9d48f9e-79f0-478c-9db0-ff7ac4a8f401,0.17,false,false,near_mint,en,USD
Strategic Betrayal,TDM,Tarkir: Dragonstorm,94,normal,uncommon,1,105145,95617742-548d-464a-bb89-a858ffa9018f,0.18,false,false,near_mint,en,USD
Opulent Palace,TDM,Tarkir: Dragonstorm,264,normal,uncommon,1,104491,21cb3b3b-0738-4c2e-a3fc-927fd6b9d3fb,0.14,false,false,near_mint,en,USD
United Battlefront,TDM,Tarkir: Dragonstorm,32,normal,rare,1,104370,dff398be-4ba4-4976-9acc-be99d2e07a61,0.51,false,false,near_mint,en,USD
Temur Battlecrier,TDM,Tarkir: Dragonstorm,228,normal,rare,1,104309,72184791-0767-4108-920c-763e92dae2d4,0.68,false,false,near_mint,en,USD
"Kotis, the Fangkeeper",TDM,Tarkir: Dragonstorm,362,normal,rare,1,104388,f70098f2-e5a8-4056-b5b3-1229fc290c51,0.48,false,false,near_mint,en,USD
Forest,TDM,Tarkir: Dragonstorm,285,normal,common,1,104317,8100bceb-ffba-487a-bb45-4fe2a156a8dc,0.06,false,false,near_mint,en,USD
Dragonfire Blade,TDM,Tarkir: Dragonstorm,240,normal,rare,1,104427,031afea3-fbfb-4663-a8cc-9b7eb7b16020,0.64,false,false,near_mint,en,USD
Great Arashin City,TDM,Tarkir: Dragonstorm,257,normal,rare,1,105033,ecba23b6-9f3a-431e-bc22-f1fb04d27b68,0.33,false,false,near_mint,en,USD
Smile at Death,TDM,Tarkir: Dragonstorm,24,normal,mythic,1,104000,ae2da18f-0d7d-446c-b463-8bf170ed95da,3.51,false,false,near_mint,en,USD
Maelstrom of the Spirit Dragon,TDM,Tarkir: Dragonstorm,260,normal,rare,1,104359,c4e90bfb-d9a5-48a9-9ff9-b0f50a813eee,1.31,false,false,near_mint,en,USD
Eshki Dragonclaw,TDM,Tarkir: Dragonstorm,182,normal,rare,1,104445,0d369c44-78ee-4f3c-bf2b-cddba7fe26d4,0.19,false,false,near_mint,en,USD
Skirmish Rhino,TDM,Tarkir: Dragonstorm,224,normal,uncommon,1,103992,4a2e9ba1-c254-41e3-9845-4e81f9fec38d,0.15,false,false,near_mint,en,USD
"Teval, Arbiter of Virtue",TDM,Tarkir: Dragonstorm,373,normal,mythic,1,104332,a19c38bc-946c-438a-ac8b-f59ff0b4c613,7.06,false,false,near_mint,en,USD
"Ureni, the Song Unending",TDM,Tarkir: Dragonstorm,233,normal,mythic,1,104253,227802c0-4ff6-43a8-a850-ed0f546dc5ac,3.79,false,false,near_mint,en,USD
Hardened Tactician,TDM,Tarkir: Dragonstorm,191,normal,uncommon,1,104780,86b225cb-5c45-4da1-a64e-b04091e483e8,1.0,false,false,near_mint,en,USD
Sandsteppe Citadel,TDM,Tarkir: Dragonstorm,266,normal,uncommon,1,104603,47f47e7f-39ba-4807-8e32-7262a61dfbba,0.13,false,false,near_mint,en,USD
"Kotis, the Fangkeeper",TDM,Tarkir: Dragonstorm,202,normal,rare,1,104364,d3736f17-f80b-4b2c-b919-2c963bc14682,0.28,false,false,near_mint,en,USD
Magmatic Hellkite,TDM,Tarkir: Dragonstorm,111,normal,rare,1,104895,b3b3aec8-d931-4c7f-86b5-1e7dfb717b59,0.56,false,false,near_mint,en,USD
Dalkovan Encampment,TDM,Tarkir: Dragonstorm,253,normal,rare,1,104822,98ad5f0c-8775-4e89-8e92-84a6ade93e35,0.38,false,false,near_mint,en,USD
Ambling Stormshell,TDM,Tarkir: Dragonstorm,37,normal,rare,1,104942,c74d4a57-0f66-4965-9ed7-f88a08aa1d15,0.18,false,false,near_mint,en,USD
Hollowmurk Siege,TDM,Tarkir: Dragonstorm,192,normal,rare,1,104668,5ac0e136-8877-4bfc-a831-2bf7b7b5ad1e,0.53,false,false,near_mint,en,USD
Avenger of the Fallen,TDM,Tarkir: Dragonstorm,73,normal,rare,1,104984,d5397366-151f-46b0-b9b2-fa4d5bd892d8,0.34,false,false,near_mint,en,USD
Mystic Monastery,TDM,Tarkir: Dragonstorm,262,normal,uncommon,1,104945,c7b8a01c-c400-47c7-8270-78902efe850e,0.11,false,false,near_mint,en,USD
Songcrafter Mage,TDM,Tarkir: Dragonstorm,225,normal,rare,1,104813,9523bc07-49e5-409c-ae6b-b28e305eef36,0.35,false,false,near_mint,en,USD
Misty Rainforest,SPG,Special Guests,111,normal,mythic,1,104321,894105c4-d3ce-4d38-855b-24aa47b112c1,32.31,false,false,near_mint,en,USD
Tempest Hawk,TDM,Tarkir: Dragonstorm,31,normal,common,3,104587,422f9453-ab12-4e3c-8c51-be87391395a1,0.92,false,false,near_mint,en,USD
Duty Beyond Death,TDM,Tarkir: Dragonstorm,10,normal,uncommon,2,104265,2e92640d-768b-4357-905f-bea017d351cc,0.33,false,false,near_mint,en,USD
Heritage Reclamation,TDM,Tarkir: Dragonstorm,145,normal,common,2,104636,4f8fee37-a050-4329-8b10-46d150e7a95e,0.2,false,false,near_mint,en,USD
1 Name Set code Set name Collector number Foil Rarity Quantity ManaBox ID Scryfall ID Purchase price Misprint Altered Condition Language Purchase price currency
2 Undergrowth Leopard TDM Tarkir: Dragonstorm 165 foil common 1 104307 67ab8f9a-b17c-452f-b4ef-a3f91909e3de 0.08 false false near_mint en USD
3 Gurmag Nightwatch TDM Tarkir: Dragonstorm 190 foil common 1 104369 de731430-6bbf-4782-953e-b69c46353959 0.03 false false near_mint en USD
4 Mystic Monastery TDM Tarkir: Dragonstorm 262 foil uncommon 1 104945 c7b8a01c-c400-47c7-8270-78902efe850e 0.21 false false near_mint en USD
5 Stormshriek Feral // Flush Out TDM Tarkir: Dragonstorm 124 foil common 1 104447 0ec92c44-7cf0-48a5-a3ca-bc633496d887 0.1 false false near_mint en USD
6 Reigning Victor TDM Tarkir: Dragonstorm 216 foil common 1 104334 a394112a-032b-4047-887a-6522cf7b83d5 0.02 false false near_mint en USD
7 Dragonbroods' Relic TDM Tarkir: Dragonstorm 140 foil uncommon 1 104569 3d634087-77ba-4543-aa7a-8a3774d69cd7 0.13 false false near_mint en USD
8 Sagu Wildling // Roost Seek TDM Tarkir: Dragonstorm 306 foil common 1 104903 b72ee8f9-5e79-4f77-ae7e-e4c274f78187 0.13 false false near_mint en USD
9 Sibsig Appraiser TDM Tarkir: Dragonstorm 56 foil common 1 105135 670c5b96-bac6-449b-a2bd-cb43750d3911 0.05 false false near_mint en USD
10 Sage of the Fang TDM Tarkir: Dragonstorm 155 foil uncommon 1 105123 1ebf4a9d-d90c-4017-9f00-fca89899f301 0.09 false false near_mint en USD
11 Snowmelt Stag TDM Tarkir: Dragonstorm 57 foil common 1 104869 a6b3b131-704a-4586-84f8-db465cd4a277 0.04 false false near_mint en USD
12 Tranquil Cove TDM Tarkir: Dragonstorm 270 foil common 1 104249 1c4efa6c-4f29-41cd-a728-bf0e479ace05 0.07 false false near_mint en USD
13 Rally the Monastery TDM Tarkir: Dragonstorm 19 foil uncommon 1 104136 b56e0037-8143-4c13-83e1-0c3f44e685ea 0.22 false false near_mint en USD
14 Dragon's Prey TDM Tarkir: Dragonstorm 79 foil common 1 104754 7a6004ff-4180-4332-8b51-960f8c7521d9 0.03 false false near_mint en USD
15 Ambling Stormshell TDM Tarkir: Dragonstorm 37 foil rare 1 104942 c74d4a57-0f66-4965-9ed7-f88a08aa1d15 0.45 false false near_mint en USD
16 Mountain TDM Tarkir: Dragonstorm 275 foil common 1 104397 fe0865ba-47c0-40bc-b0c6-e1ea5ae08a98 1.83 false false near_mint en USD
17 Mardu Devotee TDM Tarkir: Dragonstorm 16 foil common 1 104366 da45e9b0-a4f6-413b-9e62-666c511eb5b0 0.09 false false near_mint en USD
18 Swiftwater Cliffs TDM Tarkir: Dragonstorm 268 foil common 1 104361 ca53fb19-b8ca-485b-af1a-5117ae54bfe3 0.11 false false near_mint en USD
19 Adorned Crocodile TDM Tarkir: Dragonstorm 69 foil common 1 105159 bb13a34b-6ac8-47cb-9e91-47106a585fc1 0.05 false false near_mint en USD
20 Dusyut Earthcarver TDM Tarkir: Dragonstorm 141 foil common 1 104352 b98ecc96-f557-479a-8685-2b5487d5b407 0.02 false false near_mint en USD
21 Knockout Maneuver TDM Tarkir: Dragonstorm 147 foil uncommon 1 105149 9d218831-2a41-46a3-8e9d-93462cae5cab 0.07 false false near_mint en USD
22 Roiling Dragonstorm TDM Tarkir: Dragonstorm 55 foil uncommon 1 104280 455f4c96-684b-4b14-bd21-6799da2e1fa7 0.22 false false near_mint en USD
23 Dragonclaw Strike TDM Tarkir: Dragonstorm 180 foil uncommon 1 105161 bc7692ef-7091-4365-85a8-1edbd374f279 0.12 false false near_mint en USD
24 Seize Opportunity TDM Tarkir: Dragonstorm 119 foil common 1 104391 f7818d28-b9a5-4341-9adc-666070b8878d 0.03 false false near_mint en USD
25 Shock Brigade TDM Tarkir: Dragonstorm 120 foil common 1 104700 66940466-8e9d-4a85-bfb0-e92189b7a121 0.11 false false near_mint en USD
26 Hardened Tactician TDM Tarkir: Dragonstorm 191 foil uncommon 1 104780 86b225cb-5c45-4da1-a64e-b04091e483e8 0.43 false false near_mint en USD
27 Stormplain Detainment TDM Tarkir: Dragonstorm 28 foil common 1 104135 39f3aab5-7b54-4b55-8114-c6f9f79c255d 0.04 false false near_mint en USD
28 Formation Breaker TDM Tarkir: Dragonstorm 143 foil uncommon 1 105136 67ab8e8f-3ef6-4339-8c66-68c5aca4867a 0.08 false false near_mint en USD
29 Duty Beyond Death TDM Tarkir: Dragonstorm 10 foil uncommon 1 104265 2e92640d-768b-4357-905f-bea017d351cc 1.11 false false near_mint en USD
30 Piercing Exhale TDM Tarkir: Dragonstorm 151 foil common 1 104891 b2a0deb9-5bc3-42d5-9e1e-5f463d176aef 0.04 false false near_mint en USD
31 Trade Route Envoy TDM Tarkir: Dragonstorm 163 foil common 1 105174 f0c89d95-d697-4cfa-9dfa-52d7adb96176 0.05 false false near_mint en USD
32 Thornwood Falls TDM Tarkir: Dragonstorm 269 foil common 1 104376 ebb502c2-5fd0-46a9-b77d-010f4a942056 0.07 false false near_mint en USD
33 Kin-Tree Nurturer TDM Tarkir: Dragonstorm 83 foil common 1 105124 2177ef64-28bf-4acf-b1f1-c1408f03c411 0.03 false false near_mint en USD
34 Rebellious Strike TDM Tarkir: Dragonstorm 20 foil common 1 104949 c9bafe19-3bd6-4da0-b3e5-e0b89262504c 0.06 false false near_mint en USD
35 Scoured Barrens TDM Tarkir: Dragonstorm 267 foil common 1 104346 b4b47b80-69ed-44b0-afa0-ca90206dc16d 0.06 false false near_mint en USD
36 Avenger of the Fallen TDM Tarkir: Dragonstorm 73 foil rare 1 104984 d5397366-151f-46b0-b9b2-fa4d5bd892d8 0.68 false false near_mint en USD
37 Dismal Backwater TDM Tarkir: Dragonstorm 254 foil common 1 104238 082b52c9-c46e-44d3-b723-546ba528e07b 0.07 false false near_mint en USD
38 Rediscover the Way TDM Tarkir: Dragonstorm 215 normal rare 1 104313 79d6decf-afd5-4e96-b87e-fd7ab7e3c068 0.19 false false near_mint en USD
39 Auroral Procession TDM Tarkir: Dragonstorm 169 normal uncommon 1 104701 672f94ad-65d6-4c7d-925d-165ef264626f 0.22 false false near_mint en USD
40 Runescale Stormbrood // Chilling Screech TDM Tarkir: Dragonstorm 316 normal uncommon 1 104733 72e8f916-5a01-4918-bcb5-7fd69fe32785 0.31 false false near_mint en USD
41 Stadium Headliner TDM Tarkir: Dragonstorm 122 normal rare 1 104552 37d4ab2a-a06a-4768-b5e1-e1def957d7f4 0.44 false false near_mint en USD
42 Nature's Rhythm TDM Tarkir: Dragonstorm 150 normal rare 1 104460 1397d904-c51d-451e-8505-7f3118acc1f6 3.08 false false near_mint en USD
43 Karakyk Guardian TDM Tarkir: Dragonstorm 198 normal uncommon 1 104859 a4c77b08-c3f6-4458-8636-f226f9843b6d 0.08 false false near_mint en USD
44 Anafenza, Unyielding Lineage TDM Tarkir: Dragonstorm 2 normal rare 1 104258 29957f49-9a6b-42f6-b2fb-b48f653ab725 0.22 false false near_mint en USD
45 Narset, Jeskai Waymaster TDM Tarkir: Dragonstorm 209 normal rare 1 103995 6b77cbc1-dbc8-44d9-aa29-15cbb19afecd 0.22 false false near_mint en USD
46 Stillness in Motion TDM Tarkir: Dragonstorm 59 normal rare 1 104864 a6289251-17e4-4987-96b9-2fb1a8f90e2a 0.17 false false near_mint en USD
47 Thunder of Unity TDM Tarkir: Dragonstorm 231 normal rare 1 104671 5c953b36-f5e4-4258-91cb-f07e799321f7 0.14 false false near_mint en USD
48 The Sibsig Ceremony TDM Tarkir: Dragonstorm 340 normal rare 1 104719 6daa156c-478f-47dd-9284-b95e82ccfd68 0.67 false false near_mint en USD
49 Tersa Lightshatter TDM Tarkir: Dragonstorm 127 normal rare 1 104825 99e96b34-b1c4-4647-a38e-2cf1aedaaace 2.31 false false near_mint en USD
50 Forest TDM Tarkir: Dragonstorm 286 normal common 1 104324 8e3e83d2-96ba-4d5c-a1ed-6c08a90b339c 0.07 false false near_mint en USD
51 Windcrag Siege TDM Tarkir: Dragonstorm 235 normal rare 1 104534 31a8329b-23a1-4c49-a579-a5da8d01435a 1.68 false false near_mint en USD
52 Mountain TDM Tarkir: Dragonstorm 284 normal common 1 104274 3df7c206-97b6-49d7-ba01-7a35fd8c61d9 0.05 false false near_mint en USD
53 Inevitable Defeat TDM Tarkir: Dragonstorm 194 normal rare 1 103997 9d677980-b608-407e-9f17-790a81263f15 0.28 false false near_mint en USD
54 Dalkovan Encampment TDM Tarkir: Dragonstorm 394 normal rare 1 104670 5af006f6-135e-4ea0-8ce4-7824934e87da 0.72 false false near_mint en USD
55 Host of the Hereafter TDM Tarkir: Dragonstorm 193 normal uncommon 1 104448 0f182957-8133-45a7-80a3-1944bead4d43 0.14 false false near_mint en USD
56 Sarkhan, Dragon Ascendant TDM Tarkir: Dragonstorm 118 normal rare 1 104003 c2200646-7b7c-489d-bbae-16b03e1d7fb2 0.32 false false near_mint en USD
57 Stormscale Scion TDM Tarkir: Dragonstorm 123 normal mythic 1 103987 0ac43386-bd32-425c-8776-cec00b064cbc 6.78 false false near_mint en USD
58 Dragon Sniper TDM Tarkir: Dragonstorm 139 normal uncommon 1 105120 074b1e00-45bb-4436-8f5e-058512b2d08a 0.25 false false near_mint en USD
59 Island TDM Tarkir: Dragonstorm 273 normal common 1 104276 4208e66c-8c98-4c48-ab07-8523c0b26ca4 1.02 false false near_mint en USD
60 Yathan Roadwatcher TDM Tarkir: Dragonstorm 236 normal rare 1 104800 8e77339b-dd82-481c-9ee2-4156ca69ad35 0.14 false false near_mint en USD
61 Nomad Outpost TDM Tarkir: Dragonstorm 263 normal uncommon 1 104868 a68fbeaa-941f-4d53-becd-f93ed22b9a54 0.12 false false near_mint en USD
62 Sage of the Skies TDM Tarkir: Dragonstorm 22 normal rare 1 104710 6ade6918-6d1d-448d-ab56-93996051e9a9 0.21 false false near_mint en USD
63 Kheru Goldkeeper TDM Tarkir: Dragonstorm 199 normal uncommon 1 104798 8d11183a-57f5-4ddb-8a6e-15fff704b114 0.18 false false near_mint en USD
64 All-Out Assault TDM Tarkir: Dragonstorm 167 normal mythic 1 104348 b74876d8-f6a6-4b47-b960-b01a331bab01 4.11 false false near_mint en USD
65 Winternight Stories TDM Tarkir: Dragonstorm 67 normal rare 1 104693 64d9367c-f50c-4568-aa63-6760c44ecaeb 0.44 false false near_mint en USD
66 New Way Forward TDM Tarkir: Dragonstorm 211 normal rare 1 104996 d9d48f9e-79f0-478c-9db0-ff7ac4a8f401 0.17 false false near_mint en USD
67 Strategic Betrayal TDM Tarkir: Dragonstorm 94 normal uncommon 1 105145 95617742-548d-464a-bb89-a858ffa9018f 0.18 false false near_mint en USD
68 Opulent Palace TDM Tarkir: Dragonstorm 264 normal uncommon 1 104491 21cb3b3b-0738-4c2e-a3fc-927fd6b9d3fb 0.14 false false near_mint en USD
69 United Battlefront TDM Tarkir: Dragonstorm 32 normal rare 1 104370 dff398be-4ba4-4976-9acc-be99d2e07a61 0.51 false false near_mint en USD
70 Temur Battlecrier TDM Tarkir: Dragonstorm 228 normal rare 1 104309 72184791-0767-4108-920c-763e92dae2d4 0.68 false false near_mint en USD
71 Kotis, the Fangkeeper TDM Tarkir: Dragonstorm 362 normal rare 1 104388 f70098f2-e5a8-4056-b5b3-1229fc290c51 0.48 false false near_mint en USD
72 Forest TDM Tarkir: Dragonstorm 285 normal common 1 104317 8100bceb-ffba-487a-bb45-4fe2a156a8dc 0.06 false false near_mint en USD
73 Dragonfire Blade TDM Tarkir: Dragonstorm 240 normal rare 1 104427 031afea3-fbfb-4663-a8cc-9b7eb7b16020 0.64 false false near_mint en USD
74 Great Arashin City TDM Tarkir: Dragonstorm 257 normal rare 1 105033 ecba23b6-9f3a-431e-bc22-f1fb04d27b68 0.33 false false near_mint en USD
75 Smile at Death TDM Tarkir: Dragonstorm 24 normal mythic 1 104000 ae2da18f-0d7d-446c-b463-8bf170ed95da 3.51 false false near_mint en USD
76 Maelstrom of the Spirit Dragon TDM Tarkir: Dragonstorm 260 normal rare 1 104359 c4e90bfb-d9a5-48a9-9ff9-b0f50a813eee 1.31 false false near_mint en USD
77 Eshki Dragonclaw TDM Tarkir: Dragonstorm 182 normal rare 1 104445 0d369c44-78ee-4f3c-bf2b-cddba7fe26d4 0.19 false false near_mint en USD
78 Skirmish Rhino TDM Tarkir: Dragonstorm 224 normal uncommon 1 103992 4a2e9ba1-c254-41e3-9845-4e81f9fec38d 0.15 false false near_mint en USD
79 Teval, Arbiter of Virtue TDM Tarkir: Dragonstorm 373 normal mythic 1 104332 a19c38bc-946c-438a-ac8b-f59ff0b4c613 7.06 false false near_mint en USD
80 Ureni, the Song Unending TDM Tarkir: Dragonstorm 233 normal mythic 1 104253 227802c0-4ff6-43a8-a850-ed0f546dc5ac 3.79 false false near_mint en USD
81 Hardened Tactician TDM Tarkir: Dragonstorm 191 normal uncommon 1 104780 86b225cb-5c45-4da1-a64e-b04091e483e8 1.0 false false near_mint en USD
82 Sandsteppe Citadel TDM Tarkir: Dragonstorm 266 normal uncommon 1 104603 47f47e7f-39ba-4807-8e32-7262a61dfbba 0.13 false false near_mint en USD
83 Kotis, the Fangkeeper TDM Tarkir: Dragonstorm 202 normal rare 1 104364 d3736f17-f80b-4b2c-b919-2c963bc14682 0.28 false false near_mint en USD
84 Magmatic Hellkite TDM Tarkir: Dragonstorm 111 normal rare 1 104895 b3b3aec8-d931-4c7f-86b5-1e7dfb717b59 0.56 false false near_mint en USD
85 Dalkovan Encampment TDM Tarkir: Dragonstorm 253 normal rare 1 104822 98ad5f0c-8775-4e89-8e92-84a6ade93e35 0.38 false false near_mint en USD
86 Ambling Stormshell TDM Tarkir: Dragonstorm 37 normal rare 1 104942 c74d4a57-0f66-4965-9ed7-f88a08aa1d15 0.18 false false near_mint en USD
87 Hollowmurk Siege TDM Tarkir: Dragonstorm 192 normal rare 1 104668 5ac0e136-8877-4bfc-a831-2bf7b7b5ad1e 0.53 false false near_mint en USD
88 Avenger of the Fallen TDM Tarkir: Dragonstorm 73 normal rare 1 104984 d5397366-151f-46b0-b9b2-fa4d5bd892d8 0.34 false false near_mint en USD
89 Mystic Monastery TDM Tarkir: Dragonstorm 262 normal uncommon 1 104945 c7b8a01c-c400-47c7-8270-78902efe850e 0.11 false false near_mint en USD
90 Songcrafter Mage TDM Tarkir: Dragonstorm 225 normal rare 1 104813 9523bc07-49e5-409c-ae6b-b28e305eef36 0.35 false false near_mint en USD
91 Misty Rainforest SPG Special Guests 111 normal mythic 1 104321 894105c4-d3ce-4d38-855b-24aa47b112c1 32.31 false false near_mint en USD
92 Tempest Hawk TDM Tarkir: Dragonstorm 31 normal common 3 104587 422f9453-ab12-4e3c-8c51-be87391395a1 0.92 false false near_mint en USD
93 Duty Beyond Death TDM Tarkir: Dragonstorm 10 normal uncommon 2 104265 2e92640d-768b-4357-905f-bea017d351cc 0.33 false false near_mint en USD
94 Heritage Reclamation TDM Tarkir: Dragonstorm 145 normal common 2 104636 4f8fee37-a050-4329-8b10-46d150e7a95e 0.2 false false near_mint en USD

View File

@ -66,8 +66,8 @@ async def lifespan(app: FastAPI):
logger.info("Most recent prices updated successfully") logger.info("Most recent prices updated successfully")
# Create default customer, vendor, and marketplace # Create default customer, vendor, and marketplace
inv_data_init = await data_init_service.initialize_inventory_data(db) #inv_data_init = await data_init_service.initialize_inventory_data(db)
logger.info(f"Inventory data initialization results: {inv_data_init}") #logger.info(f"Inventory data initialization results: {inv_data_init}")
# Start the scheduler # Start the scheduler
scheduler = service_manager.get_service('scheduler') scheduler = service_manager.get_service('scheduler')
await scheduler.refresh_tcgplayer_inventory_table(db) await scheduler.refresh_tcgplayer_inventory_table(db)
@ -115,6 +115,46 @@ async def read_app_js():
raise HTTPException(status_code=404, detail="App.js file not found") raise HTTPException(status_code=404, detail="App.js file not found")
return FileResponse(js_path) return FileResponse(js_path)
# Serve manabox.html
@app.get("/manabox.html")
async def read_manabox_html():
html_path = Path('app/static/manabox.html')
if not html_path.exists():
raise HTTPException(status_code=404, detail="Manabox.html file not found")
return FileResponse(html_path)
# Serve manabox.js
@app.get("/manabox.js")
async def read_manabox_js():
js_path = Path('app/static/manabox.js')
if not js_path.exists():
raise HTTPException(status_code=404, detail="Manabox.js file not found")
return FileResponse(js_path)
# serve transactions.html
@app.get("/transactions.html")
async def read_transactions_html():
html_path = Path('app/static/transactions.html')
if not html_path.exists():
raise HTTPException(status_code=404, detail="Transaction.html file not found")
return FileResponse(html_path)
# serve transactions.js
@app.get("/transactions.js")
async def read_transactions_js():
js_path = Path('app/static/transactions.js')
if not js_path.exists():
raise HTTPException(status_code=404, detail="Transaction.js file not found")
return FileResponse(js_path)
# serve styles.css
@app.get("/styles.css")
async def read_styles_css():
css_path = Path('app/static/styles.css')
if not css_path.exists():
raise HTTPException(status_code=404, detail="Styles.css file not found")
return FileResponse(css_path)
# Configure CORS with specific origins in production # Configure CORS with specific origins in production
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View File

@ -252,6 +252,9 @@ class Vendor(Base):
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
deleted_at = Column(DateTime(timezone=True), nullable=True) deleted_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
transactions = relationship("Transaction", back_populates="vendors")
class Customer(Base): class Customer(Base):
__tablename__ = "customers" __tablename__ = "customers"
@ -260,6 +263,10 @@ class Customer(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
deleted_at = Column(DateTime(timezone=True), nullable=True) deleted_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
transactions = relationship("Transaction", back_populates="customers")
class Transaction(Base): class Transaction(Base):
__tablename__ = "transactions" __tablename__ = "transactions"
@ -277,6 +284,9 @@ class Transaction(Base):
# Relationships # Relationships
transaction_items = relationship("TransactionItem", back_populates="transaction") transaction_items = relationship("TransactionItem", back_populates="transaction")
vendors = relationship("Vendor", back_populates="transactions")
customers = relationship("Customer", back_populates="transactions")
marketplaces = relationship("Marketplace", back_populates="transactions")
class Marketplace(Base): class Marketplace(Base):
__tablename__ = "marketplaces" __tablename__ = "marketplaces"
@ -289,7 +299,7 @@ class Marketplace(Base):
# Relationships # Relationships
listings = relationship("MarketplaceListing", back_populates="marketplace") listings = relationship("MarketplaceListing", back_populates="marketplace")
transactions = relationship("Transaction", back_populates="marketplaces")
class MarketplaceListing(Base): class MarketplaceListing(Base):
__tablename__ = "marketplace_listings" __tablename__ = "marketplace_listings"

View File

@ -1,10 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from datetime import datetime
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_, func
from app.db.database import get_db from app.db.database import get_db
from app.services.service_manager import ServiceManager from app.services.service_manager import ServiceManager
from app.contexts.inventory_item import InventoryItemContextFactory from app.contexts.inventory_item import InventoryItemContextFactory
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, SealedExpectedValueCreate, GetAllTransactionsResponse, TransactionResponse, TransactionItemResponse, InventoryItemResponse, TCGPlayerProductResponse, OpenEventResponse, OpenEventCreate, OpenEventResultingItemsResponse, OpenEventsForInventoryItemResponse
from app.models.inventory_management import Transaction
from app.models.tcgplayer_products import TCGPlayerProduct
from typing import List from typing import List
from fastapi.responses import StreamingResponse
router = APIRouter(prefix="/inventory") router = APIRouter(prefix="/inventory")
service_manager = ServiceManager() service_manager = ServiceManager()
@ -155,6 +161,14 @@ async def create_vendor(
vendor = await inventory_service.create_vendor(db, vendor_name) vendor = await inventory_service.create_vendor(db, vendor_name)
return vendor return vendor
@router.get("/vendors")
async def get_vendors(
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
vendors = await inventory_service.get_vendors(db)
return vendors
@router.post("/marketplaces") @router.post("/marketplaces")
async def create_marketplace( async def create_marketplace(
marketplace_name: str, marketplace_name: str,
@ -162,4 +176,313 @@ async def create_marketplace(
): ):
inventory_service = service_manager.get_service("inventory") inventory_service = service_manager.get_service("inventory")
marketplace = await inventory_service.create_marketplace(db, marketplace_name) marketplace = await inventory_service.create_marketplace(db, marketplace_name)
return marketplace return marketplace
@router.get("/marketplaces")
async def get_marketplaces(
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
marketplaces = await inventory_service.get_marketplaces(db)
return marketplaces
@router.get("/products/search")
async def get_products(q: str, db: Session = Depends(get_db)):
query = ' & '.join(q.lower().split()) # This ensures all terms must match
products = db.query(TCGPlayerProduct).filter(
func.to_tsvector('english', TCGPlayerProduct.name)
.op('@@')(func.to_tsquery('english', query))
).all()
return products
@router.get("/products/{product_id}/expected-value")
async def get_expected_value(
product_id: int,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
expected_value = await inventory_service.get_expected_value(db, product_id)
return expected_value
@router.post("/products/expected-value")
async def create_expected_value(
expected_value_data: SealedExpectedValueCreate,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
expected_value = await inventory_service.create_expected_value(db, expected_value_data)
return expected_value
@router.post("/transactions/purchase")
async def create_purchase_transaction(
transaction_data: PurchaseTransactionCreate,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
transaction = await inventory_service.create_purchase_transaction(db, transaction_data)
return transaction
@router.get("/transactions")
async def get_transactions(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100
):
inventory_service = service_manager.get_service("inventory")
total = db.query(func.count(Transaction.id)).scalar()
transactions = await inventory_service.get_transactions(db, skip, limit)
return GetAllTransactionsResponse(
total=total,
transactions=[TransactionResponse(
id=transaction.id,
vendor_id=transaction.vendor_id,
customer_id=transaction.customer_id,
marketplace_id=transaction.marketplace_id,
transaction_type=transaction.transaction_type,
transaction_date=transaction.transaction_date,
transaction_total_amount=transaction.transaction_total_amount,
transaction_notes=transaction.transaction_notes,
created_at=transaction.created_at,
updated_at=transaction.updated_at,
transaction_items=[TransactionItemResponse(
id=transaction_item.id,
transaction_id=transaction_item.transaction_id,
inventory_item_id=transaction_item.inventory_item_id,
unit_price=transaction_item.unit_price,
created_at=transaction_item.created_at,
updated_at=transaction_item.updated_at,
deleted_at=transaction_item.deleted_at
) for transaction_item in transaction.transaction_items]
) for transaction in transactions]
)
@router.get("/transactions/{transaction_id}")
async def get_transaction(
transaction_id: int,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
transaction = await inventory_service.get_transaction(db, transaction_id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
return TransactionResponse(
id=transaction.id,
vendor_id=transaction.vendor_id,
customer_id=transaction.customer_id,
marketplace_id=transaction.marketplace_id,
transaction_type=transaction.transaction_type,
transaction_date=transaction.transaction_date,
transaction_total_amount=transaction.transaction_total_amount,
transaction_notes=transaction.transaction_notes,
created_at=transaction.created_at,
updated_at=transaction.updated_at,
transaction_items=[TransactionItemResponse(
id=transaction_item.id,
transaction_id=transaction_item.transaction_id,
inventory_item_id=transaction_item.inventory_item_id,
unit_price=transaction_item.unit_price,
created_at=transaction_item.created_at,
updated_at=transaction_item.updated_at,
deleted_at=transaction_item.deleted_at
) for transaction_item in transaction.transaction_items]
)
@router.get("/items/{inventory_item_id}")
async def get_inventory_item(
inventory_item_id: int,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
marketplace_listing_service = service_manager.get_service("marketplace_listing")
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
marketplace_listing = await marketplace_listing_service.get_marketplace_listing(db, inventory_item, marketplace)
if marketplace_listing is None:
listed_price = None
recommended_price = None
marketplace_listing_id = None
else:
if marketplace_listing.listed_price is not None:
listed_price = marketplace_listing.listed_price.price if marketplace_listing.listed_price.price is not None else None
else:
listed_price = None
if marketplace_listing.recommended_price is not None:
recommended_price = marketplace_listing.recommended_price.price if marketplace_listing.recommended_price.price is not None else None
else:
recommended_price = None
marketplace_listing_id = marketplace_listing.id
return InventoryItemResponse(
id=inventory_item.id,
physical_item_id=inventory_item.physical_item_id,
cost_basis=inventory_item.cost_basis,
parent_id=inventory_item.parent_id,
created_at=inventory_item.created_at,
updated_at=inventory_item.updated_at,
item_type=inventory_item.physical_item.item_type,
listed_price=listed_price,
recommended_price=recommended_price,
marketplace_listing_id=marketplace_listing_id,
product=TCGPlayerProductResponse(
id=inventory_item.physical_item.product_direct.id,
tcgplayer_product_id=inventory_item.physical_item.product_direct.tcgplayer_product_id,
name=inventory_item.physical_item.product_direct.name,
image_url=inventory_item.physical_item.product_direct.image_url,
category_id=inventory_item.physical_item.product_direct.category_id,
group_id=inventory_item.physical_item.product_direct.group_id,
url=inventory_item.physical_item.product_direct.url,
market_price=inventory_item.physical_item.product_direct.most_recent_tcgplayer_price.market_price,
category_name=inventory_item.physical_item.product_direct.category.name,
group_name=inventory_item.physical_item.product_direct.group.name
)
)
@router.post("/items/{inventory_item_id}/open")
async def open_box_or_case(
open_event_data: OpenEventCreate,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
inventory_item = await inventory_service.get_inventory_item(db, open_event_data.inventory_item_id)
file_service = service_manager.get_service("file")
files = [await file_service.get_file(db, file_id) for file_id in open_event_data.manabox_file_upload_ids]
if inventory_item.physical_item.item_type == "box":
box_service = service_manager.get_service("box")
open_event = await box_service.open_box(db, inventory_item.physical_item, files)
return OpenEventResponse(
id=open_event.id,
source_item_id=open_event.source_item_id,
created_at=open_event.created_at,
updated_at=open_event.updated_at
)
elif inventory_item.physical_item.item_type == "case":
case_service = service_manager.get_service("case")
open_event = await case_service.open_case(db, inventory_item.physical_item, files)
return OpenEventResponse(
id=open_event.id,
source_item_id=open_event.source_item_id,
created_at=open_event.created_at,
updated_at=open_event.updated_at
)
else:
raise HTTPException(status_code=400, detail="Invalid item type")
@router.get("/items/{inventory_item_id}/open-events")
async def get_open_events(
inventory_item_id: int,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
# Don't return open events for cards
if inventory_item.physical_item.item_type == 'card':
return OpenEventsForInventoryItemResponse(open_events=[])
open_events = await inventory_service.get_open_events_for_inventory_item(db, inventory_item)
return OpenEventsForInventoryItemResponse(
open_events=[OpenEventResponse(
id=open_event.id,
source_item_id=open_event.source_item_id,
created_at=open_event.created_at,
updated_at=open_event.updated_at
) for open_event in open_events]
)
@router.get("/items/{inventory_item_id}/open-events/{open_event_id}/resulting-items", response_model=List[InventoryItemResponse])
async def get_resulting_items(
inventory_item_id: int,
open_event_id: int,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
resulting_items = await inventory_service.get_resulting_items_for_open_event(db, open_event)
marketplace_listing_service = service_manager.get_service("marketplace_listing")
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
marketplace_listing = await marketplace_listing_service.get_marketplace_listing(db, inventory_item, marketplace)
if marketplace_listing is None:
listed_price = None
recommended_price = None
marketplace_listing_id = None
else:
if marketplace_listing.listed_price is not None:
listed_price = marketplace_listing.listed_price.price if marketplace_listing.listed_price.price is not None else None
else:
listed_price = None
if marketplace_listing.recommended_price is not None:
recommended_price = marketplace_listing.recommended_price.price if marketplace_listing.recommended_price.price is not None else None
else:
recommended_price = None
marketplace_listing_id = marketplace_listing.id
return [InventoryItemResponse(
id=resulting_item.id,
physical_item_id=resulting_item.physical_item_id,
cost_basis=resulting_item.cost_basis,
parent_id=resulting_item.parent_id,
product=TCGPlayerProductResponse(
id=resulting_item.physical_item.product_direct.id,
tcgplayer_product_id=resulting_item.physical_item.product_direct.tcgplayer_product_id,
name=resulting_item.physical_item.product_direct.name,
image_url=resulting_item.physical_item.product_direct.image_url,
category_id=resulting_item.physical_item.product_direct.category_id,
group_id=resulting_item.physical_item.product_direct.group_id,
url=resulting_item.physical_item.product_direct.url,
market_price=resulting_item.physical_item.product_direct.most_recent_tcgplayer_price.market_price,
category_name=resulting_item.physical_item.product_direct.category.name,
group_name=resulting_item.physical_item.product_direct.group.name
),
item_type=resulting_item.physical_item.item_type,
marketplace_listing_id=marketplace_listing_id,
listed_price=listed_price,
recommended_price=recommended_price,
created_at=resulting_item.created_at,
updated_at=resulting_item.updated_at) for resulting_item in resulting_items]
@router.post("/items/{inventory_item_id}/open-events/{open_event_id}/create-listings")
async def create_marketplace_listings(
inventory_item_id: int,
open_event_id: int,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
resulting_items = await inventory_service.get_resulting_items_for_open_event(db, open_event)
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
marketplace_listing_service = service_manager.get_service("marketplace_listing")
for resulting_item in resulting_items:
await marketplace_listing_service.create_marketplace_listing(db, resulting_item, marketplace)
return {"message": f"{len(resulting_items)} marketplace listings created successfully"}
@router.post("/items/{inventory_item_id}/open-events/{open_event_id}/confirm-listings")
async def confirm_listings(
inventory_item_id: int,
open_event_id: int,
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
marketplace_listing_service = service_manager.get_service("marketplace_listing")
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
try:
csv_string = await marketplace_listing_service.confirm_listings(db, open_event, marketplace)
if not csv_string:
raise ValueError("No CSV data generated")
# Create a streaming response with the CSV data
return StreamingResponse(
iter([csv_string]),
media_type="text/csv",
headers={
"Content-Disposition": f"attachment; filename=tcgplayer_add_file_{open_event.id}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv"
}
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form, BackgroundTasks
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.db.database import get_db from app.db.database import get_db
from app.services.service_manager import ServiceManager from app.services.service_manager import ServiceManager
@ -50,6 +50,7 @@ def is_valid_csv(file: UploadFile) -> tuple[bool, str]:
@router.post("/process-csv") @router.post("/process-csv")
async def process_manabox_csv( async def process_manabox_csv(
background_tasks: BackgroundTasks,
file: UploadFile = File(...), file: UploadFile = File(...),
source: str = Form(...), source: str = Form(...),
description: str = Form(...), description: str = Form(...),
@ -72,7 +73,7 @@ async def process_manabox_csv(
manabox_service = service_manager.get_service("manabox") manabox_service = service_manager.get_service("manabox")
success = await manabox_service.process_manabox_csv(db, content, metadata) success = await manabox_service.process_manabox_csv(db, content, metadata, background_tasks)
if not success: if not success:
raise HTTPException(status_code=400, detail="Failed to process CSV file") raise HTTPException(status_code=400, detail="Failed to process CSV file")

View File

@ -220,6 +220,7 @@ async def print_pirate_ship_label(
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to print Pirate Ship label: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to print Pirate Ship label: {str(e)}")
# what even is this TODO delete
@router.post("/process-manabox-csv") @router.post("/process-manabox-csv")
async def process_manabox_csv( async def process_manabox_csv(
file: UploadFile = File(...), file: UploadFile = File(...),

0
app/schemas/inventory.py Normal file
View File

View File

@ -1,12 +1,12 @@
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime from datetime import datetime
from app.models.tcgplayer_products import TCGPlayerProduct
class PurchaseItem(BaseModel): class PurchaseItem(BaseModel):
product_id: int product_id: int
unit_price: float unit_price: float
quantity: int quantity: int
is_case: bool item_type: str
num_boxes: Optional[int] = None num_boxes: Optional[int] = None
# TODO: remove is_case and num_boxes, should derive from product_id # TODO: remove is_case and num_boxes, should derive from product_id
@ -30,11 +30,11 @@ class SaleTransactionCreate(BaseModel):
class TransactionItemResponse(BaseModel): class TransactionItemResponse(BaseModel):
id: int id: int
transaction_id: int transaction_id: int
physical_item_id: int inventory_item_id: int
unit_price: float unit_price: float
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
deleted_at: Optional[datetime] = None
class TransactionResponse(BaseModel): class TransactionResponse(BaseModel):
id: int id: int
vendor_id: Optional[int] = None vendor_id: Optional[int] = None
@ -46,4 +46,64 @@ class TransactionResponse(BaseModel):
transaction_notes: Optional[str] = None transaction_notes: Optional[str] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
transaction_items: List[TransactionItemResponse] deleted_at: Optional[datetime] = None
transaction_items: List[TransactionItemResponse]
class GetAllTransactionsResponse(BaseModel):
total: int
transactions: List[TransactionResponse]
class SealedExpectedValueResponse(BaseModel):
id: int
tcgplayer_product_id: int
expected_value: float
class SealedExpectedValueCreate(BaseModel):
tcgplayer_product_id: int
expected_value: float
class TCGPlayerProductResponse(BaseModel):
id: int
tcgplayer_product_id: int
name: str
image_url: str
category_id: int
group_id: int
url: str
market_price: float
category_name: str
group_name: str
class InventoryItemResponse(BaseModel):
id: int
physical_item_id: int
cost_basis: float
item_type: str
listed_price: Optional[float] = None
marketplace_listing_id: Optional[int] = None
recommended_price: Optional[float] = None
parent_id: Optional[int] = None
created_at: datetime
updated_at: datetime
product: Optional[TCGPlayerProductResponse] = None
class OpenEventResponse(BaseModel):
id: int
source_item_id: int
created_at: datetime
updated_at: datetime
class OpenEventCreate(BaseModel):
inventory_item_id: int
manabox_file_upload_ids: List[int]
class OpenEventResultingItemsResponse(BaseModel):
id: int
source_item_id: int
created_at: datetime
updated_at: datetime
resulting_items: List[InventoryItemResponse]
class OpenEventsForInventoryItemResponse(BaseModel):
open_events: List[OpenEventResponse]

View File

@ -772,7 +772,7 @@ class DataInitializationService(BaseService):
transaction = await inventory_service.create_purchase_transaction(db, PurchaseTransactionCreate( transaction = await inventory_service.create_purchase_transaction(db, PurchaseTransactionCreate(
vendor_id=vendor.id, vendor_id=vendor.id,
transaction_date=datetime.now(), transaction_date=datetime.now(),
items=[PurchaseItem(product_id=619645, unit_price=100, quantity=1, is_case=False)], items=[PurchaseItem(product_id=619645, unit_price=100, quantity=1, item_type="box")],
transaction_notes="tdm real box test" transaction_notes="tdm real box test"
#PurchaseItem(product_id=562119, unit_price=800.01, quantity=2, is_case=True, num_boxes=6)], #PurchaseItem(product_id=562119, unit_price=800.01, quantity=2, is_case=True, num_boxes=6)],
#transaction_notes="Test Transaction: 1 case and 2 boxes of foundations" #transaction_notes="Test Transaction: 1 case and 2 boxes of foundations"
@ -784,7 +784,7 @@ class DataInitializationService(BaseService):
if item.inventory_item.physical_item.item_type == "box": if item.inventory_item.physical_item.item_type == "box":
manabox_service = self.get_service("manabox") manabox_service = self.get_service("manabox")
#file_path = 'app/data/test_data/manabox_test_file.csv' #file_path = 'app/data/test_data/manabox_test_file.csv'
file_path = 'app/data/test_data/tdmtest.csv' file_path = 'app/data/test_data/dragon.csv'
file_bytes = open(file_path, 'rb').read() file_bytes = open(file_path, 'rb').read()
manabox_file = await manabox_service.process_manabox_csv(db, file_bytes, {"source": "test", "description": "test"}, wait=True) manabox_file = await manabox_service.process_manabox_csv(db, file_bytes, {"source": "test", "description": "test"}, wait=True)
# Ensure manabox_file is a list before passing it # Ensure manabox_file is a list before passing it

View File

@ -120,7 +120,7 @@ class FileService:
"""List files with optional filtering""" """List files with optional filtering"""
query = db.query(File) query = db.query(File)
if file_type: if file_type:
query = query.filter(File.type == file_type).order_by(File.created_at.desc()) query = query.filter(File.file_type == file_type).order_by(File.created_at.desc())
files = query.offset(skip).limit(limit).all() files = query.offset(skip).limit(limit).all()
return [FileInDB.model_validate(file) for file in files] return [FileInDB.model_validate(file) for file in files]

View File

@ -1,14 +1,16 @@
from typing import List, Optional, Dict, TypedDict from typing import List, Optional, Dict, TypedDict
from sqlalchemy.orm import Session from sqlalchemy.orm import Session, joinedload
from decimal import Decimal
from app.services.base_service import BaseService from app.services.base_service import BaseService
from app.models.manabox_import_staging import ManaboxImportStaging from app.models.manabox_import_staging import ManaboxImportStaging
from app.contexts.inventory_item import InventoryItemContextFactory from app.contexts.inventory_item import InventoryItemContextFactory
from app.models.inventory_management import ( from app.models.inventory_management import (
OpenEvent, Card, InventoryItem, Case, OpenEvent, Card, InventoryItem, Case, SealedExpectedValue,
Transaction, TransactionItem, Customer, Vendor, Marketplace, Box, MarketplaceListing Transaction, TransactionItem, Customer, Vendor, Marketplace, Box, MarketplaceListing
) )
from app.schemas.file import FileInDB from app.schemas.file import FileInDB
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse from app.models.inventory_management import PhysicalItem
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse, SealedExpectedValueCreate
from app.db.database import transaction as db_transaction from app.db.database import transaction as db_transaction
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@ -20,6 +22,58 @@ class InventoryService(BaseService):
def __init__(self): def __init__(self):
super().__init__(None) super().__init__(None)
async def get_resulting_items_for_open_event(self, db: Session, open_event: OpenEvent) -> List[InventoryItem]:
# Get the IDs of resulting items
resulting_item_ids = [item.id for item in open_event.resulting_items]
# Query using the IDs
return db.query(InventoryItem).filter(InventoryItem.physical_item_id.in_(resulting_item_ids)).all()
async def get_open_event(self, db: Session, inventory_item: InventoryItem, open_event_id: int) -> OpenEvent:
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).filter(OpenEvent.id == open_event_id).first()
async def get_open_events_for_inventory_item(self, db: Session, inventory_item: InventoryItem) -> List[OpenEvent]:
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).all()
async def get_inventory_item(self, db: Session, inventory_item_id: int) -> InventoryItem:
return db.query(InventoryItem)\
.options(
joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct)
)\
.filter(InventoryItem.id == inventory_item_id)\
.first()
async def get_expected_value(self, db: Session, product_id: int) -> float:
expected_value = db.query(SealedExpectedValue).filter(SealedExpectedValue.tcgplayer_product_id == product_id).first()
return expected_value.expected_value if expected_value else None
async def get_transactions(self, db: Session, skip: int, limit: int) -> List[Transaction]:
return db.query(Transaction)\
.order_by(Transaction.transaction_date.desc())\
.offset(skip)\
.limit(limit)\
.all()
async def get_transaction(self, db: Session, transaction_id: int) -> Transaction:
return db.query(Transaction)\
.options(
joinedload(Transaction.transaction_items).joinedload(TransactionItem.inventory_item).joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct),
joinedload(Transaction.vendors),
joinedload(Transaction.customers),
joinedload(Transaction.marketplaces)
)\
.filter(Transaction.id == transaction_id)\
.first()
async def create_expected_value(self, db: Session, expected_value_data: SealedExpectedValueCreate) -> SealedExpectedValue:
with db_transaction(db):
expected_value = SealedExpectedValue(
tcgplayer_product_id=expected_value_data.tcgplayer_product_id,
expected_value=expected_value_data.expected_value
)
db.add(expected_value)
db.flush()
return expected_value
async def create_purchase_transaction( async def create_purchase_transaction(
self, self,
db: Session, db: Session,
@ -52,7 +106,7 @@ class InventoryService(BaseService):
# Create the physical item based on type # Create the physical item based on type
# TODO: remove is_case and num_boxes, should derive from product_id # TODO: remove is_case and num_boxes, should derive from product_id
# TODO: add support for purchasing single cards # TODO: add support for purchasing single cards
if item.is_case: if item.item_type == "case":
for i in range(item.quantity): for i in range(item.quantity):
physical_item = await case_service.create_case( physical_item = await case_service.create_case(
db=db, db=db,
@ -61,7 +115,7 @@ class InventoryService(BaseService):
num_boxes=item.num_boxes num_boxes=item.num_boxes
) )
physical_items.append(physical_item) physical_items.append(physical_item)
else: elif item.item_type == "box":
for i in range(item.quantity): for i in range(item.quantity):
physical_item = await box_service.create_box( physical_item = await box_service.create_box(
db=db, db=db,
@ -69,6 +123,9 @@ class InventoryService(BaseService):
cost_basis=item.unit_price cost_basis=item.unit_price
) )
physical_items.append(physical_item) physical_items.append(physical_item)
else:
raise ValueError(f"Invalid item type: {item.item_type}")
# TODO: add support for purchasing single cards
for physical_item in physical_items: for physical_item in physical_items:
# Create transaction item # Create transaction item
@ -128,6 +185,12 @@ class InventoryService(BaseService):
except Exception as e: except Exception as e:
raise e raise e
async def get_vendors(
self,
db: Session
) -> List[Vendor]:
return db.query(Vendor).all()
async def create_marketplace( async def create_marketplace(
self, self,
db: Session, db: Session,
@ -149,6 +212,12 @@ class InventoryService(BaseService):
except Exception as e: except Exception as e:
raise e raise e
async def get_marketplaces(
self,
db: Session
) -> List[Marketplace]:
return db.query(Marketplace).all()
class BoxService(BaseService[Box]): class BoxService(BaseService[Box]):
def __init__(self): def __init__(self):
@ -329,3 +398,106 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
except Exception as e: except Exception as e:
raise e raise e
async def get_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).order_by(MarketplaceListing.created_at.desc()).first()
async def get_active_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).filter(MarketplaceListing.listing_date != None).order_by(MarketplaceListing.created_at.desc()).first()
async def confirm_listings(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
tcgplayer_add_file = await self.create_tcgplayer_add_file(db, open_event, marketplace)
if not tcgplayer_add_file:
raise ValueError("No TCGplayer add file created")
with db_transaction(db):
for resulting_item in open_event.resulting_items:
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
if marketplace_listing is None:
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
marketplace_listing.listing_date = datetime.now()
db.flush()
return tcgplayer_add_file
async def create_tcgplayer_add_file(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
# TCGplayer Id,Product Line,Set Name,Product Name,Title,Number,Rarity,Condition,TCG Market Price,TCG Direct Low,TCG Low Price With Shipping,TCG Low Price,Total Quantity,Add to Quantity,TCG Marketplace Price,Photo URL
headers = [
"TCGplayer Id",
"Product Line",
"Set Name",
"Product Name",
"Title",
"Number",
"Rarity",
"Condition",
"TCG Market Price",
"TCG Direct Low",
"TCG Low Price With Shipping",
"TCG Low Price",
"Total Quantity",
"Add to Quantity",
"TCG Marketplace Price",
"Photo URL"
]
data = {}
for resulting_item in open_event.resulting_items:
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
if marketplace_listing is None:
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
tcgplayer_sku_id = resulting_item.tcgplayer_sku_id
if tcgplayer_sku_id in data:
data[tcgplayer_sku_id]["Add to Quantity"] += 1
continue
product_line = resulting_item.products.category.name
set_name = resulting_item.products.group.name
product_name = resulting_item.products.name
title = ""
number = resulting_item.products.ext_number
rarity = resulting_item.products.ext_rarity
condition = " ".join([condition.capitalize() for condition in resulting_item.sku.condition.split(" ")]) + (" " + resulting_item.products.sub_type_name if resulting_item.products.sub_type_name == "Foil" else "")
tcg_market_price = resulting_item.products.most_recent_tcgplayer_price.market_price
tcg_direct_low = resulting_item.products.most_recent_tcgplayer_price.direct_low_price
tcg_low_price_with_shipping = resulting_item.products.most_recent_tcgplayer_price.low_price
tcg_low_price = resulting_item.products.most_recent_tcgplayer_price.low_price
total_quantity = ""
add_to_quantity = 1
# get average recommended price of product
# get inventory items with same tcgplayer_product_id
inventory_items = db.query(InventoryItem).filter(InventoryItem.physical_item.has(tcgplayer_sku_id=tcgplayer_sku_id)).all()
inventory_item_ids = [inventory_item.id for inventory_item in inventory_items]
logger.debug(f"inventory_item_ids: {inventory_item_ids}")
valid_listings = db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item_id.in_(inventory_item_ids)).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.listing_date == None).all()
logger.debug(f"valid_listings: {valid_listings}")
avg_recommended_price = sum([listing.recommended_price.price for listing in valid_listings]) / len(valid_listings)
data[tcgplayer_sku_id] = {
"TCGplayer Id": tcgplayer_sku_id,
"Product Line": product_line,
"Set Name": set_name,
"Product Name": product_name,
"Title": title,
"Number": number,
"Rarity": rarity,
"Condition": condition,
"TCG Market Price": tcg_market_price,
"TCG Direct Low": tcg_direct_low,
"TCG Low Price With Shipping": tcg_low_price_with_shipping,
"TCG Low Price": tcg_low_price,
"Total Quantity": total_quantity,
"Add to Quantity": add_to_quantity,
"TCG Marketplace Price": f"{Decimal(avg_recommended_price):.2f}",
"Photo URL": ""
}
# format data into csv
# header
header_row = ",".join(headers)
# data
def escape_csv_value(value):
if value is None:
return ""
value = str(value)
if any(c in value for c in [',', '"', '\n']):
return f'"{value.replace('"', '""')}"'
return value
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
csv_data = "\n".join([header_row] + data_rows)
return csv_data

View File

@ -9,7 +9,7 @@ from typing import Dict, Any, Union, List
import csv import csv
import logging import logging
from datetime import datetime from datetime import datetime
import asyncio from fastapi import BackgroundTasks
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,7 +17,7 @@ class ManaboxService(BaseService):
def __init__(self): def __init__(self):
super().__init__(None) super().__init__(None)
async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], wait: bool = False) -> Union[bool, List[FileInDB]]: async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], background_tasks: BackgroundTasks, wait: bool = False) -> Union[bool, List[FileInDB]]:
# save file # save file
file = await self.file_service.save_file( file = await self.file_service.save_file(
db=db, db=db,
@ -29,34 +29,36 @@ class ManaboxService(BaseService):
metadata=metadata metadata=metadata
) )
# Create the background task
task = asyncio.create_task(self._process_file_background(db, file))
# If wait is True, wait for the task to complete and return the file
if wait: if wait:
await task await self._process_file_background(db, file)
return_value = await self.file_service.get_file(db, file.id) return_value = await self.file_service.get_file(db, file.id)
return [return_value] if return_value else [] return [return_value] if return_value else []
else:
return True background_tasks.add_task(self._process_file_background, db, file)
return True
async def _process_file_background(self, db: Session, file: FileInDB): async def _process_file_background(self, db: Session, file: FileInDB):
try: try:
# Read the CSV file # Read the CSV file
with open(file.path, 'r') as csv_file: with open(file.path, 'r') as csv_file:
reader = csv.DictReader(csv_file) reader = csv.DictReader(csv_file)
logger.debug(f"Processing file: {file.path}")
# Pre-fetch all MTGJSONCards for Scryfall IDs in the file # Pre-fetch all MTGJSONCards for Scryfall IDs in the file
scryfall_ids = {row['Scryfall ID'] for row in reader} scryfall_ids = {row['Scryfall ID'] for row in reader}
mtg_json_map = {card.scryfall_id: card for card in db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id.in_(scryfall_ids)).all()} mtg_json_map = {card.scryfall_id: card for card in db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id.in_(scryfall_ids)).all()}
logger.debug(f"len ids: {len(scryfall_ids)}")
# Re-read the file to process the rows # Re-read the file to process the rows
csv_file.seek(0) csv_file.seek(0)
logger.debug(f"header: {reader.fieldnames}")
next(reader) # Skip the header row next(reader) # Skip the header row
staging_entries = [] # To collect all staging entries for batch insert staging_entries = [] # To collect all staging entries for batch insert
critical_errors = [] # To collect errors for logging critical_errors = [] # To collect errors for logging
for row in reader: for row in reader:
logger.debug(f"Processing row: {row}")
mtg_json = mtg_json_map.get(row['Scryfall ID']) mtg_json = mtg_json_map.get(row['Scryfall ID'])
if not mtg_json: if not mtg_json:
@ -109,6 +111,7 @@ class ManaboxService(BaseService):
# Prepare the staging entry # Prepare the staging entry
quantity = int(row['Quantity']) quantity = int(row['Quantity'])
logger.debug(f"inserting row file id: {file.id} tcgplayer_product_id: {tcgplayer_product.tcgplayer_product_id} tcgplayer_sku_id: {tcgplayer_sku.tcgplayer_sku_id} quantity: {quantity}")
staging_entries.append(ManaboxImportStaging( staging_entries.append(ManaboxImportStaging(
file_id=file.id, file_id=file.id,
tcgplayer_product_id=tcgplayer_product.tcgplayer_product_id, tcgplayer_product_id=tcgplayer_product.tcgplayer_product_id,
@ -118,10 +121,13 @@ class ManaboxService(BaseService):
# Bulk insert all valid ManaboxImportStaging entries # Bulk insert all valid ManaboxImportStaging entries
if staging_entries: if staging_entries:
db.bulk_save_objects(staging_entries) logger.debug(f"inserting {len(staging_entries)} rows")
with transaction(db):
db.bulk_save_objects(staging_entries)
# Log any critical errors that occurred # Log any critical errors that occurred
for error_message in critical_errors: for error_message in critical_errors:
logger.debug(f"logging critical error: {error_message}")
with transaction(db): with transaction(db):
critical_error_log = CriticalErrorLog(error_message=error_message) critical_error_log = CriticalErrorLog(error_message=error_message)
db.add(critical_error_log) db.add(critical_error_log)

View File

@ -1,11 +1,14 @@
import logging import logging
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.services.base_service import BaseService from app.services.base_service import BaseService
from app.models.inventory_management import InventoryItem from app.models.inventory_management import InventoryItem, MarketplaceListing
from app.models.tcgplayer_inventory import TCGPlayerInventory from app.models.tcgplayer_inventory import TCGPlayerInventory
from app.models.pricing import PricingEvent from app.models.pricing import PricingEvent
from app.db.database import transaction from app.db.database import transaction
from decimal import Decimal from decimal import Decimal
from datetime import datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -148,8 +151,32 @@ class PricingService(BaseService):
free_shipping_adjustment=free_shipping_adjustment free_shipping_adjustment=free_shipping_adjustment
) )
db.add(pricing_event) db.add(pricing_event)
# delete previous pricing events for inventory item
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
return pricing_event return pricing_event
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float: def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
pass pass
def update_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> list[PricingEvent]:
# get inventory items for sku
updated_prices = []
inventory_items = db.query(InventoryItem).filter(
InventoryItem.physical_item.tcgplayer_sku_id == tcgplayer_sku_id
).all()
for inventory_item in inventory_items:
pricing_event = self.set_price(db, inventory_item)
updated_prices.append(pricing_event)
return updated_prices
def set_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> float:
# update price for all inventory items for sku
prices = self.update_price_for_product(db, tcgplayer_sku_id)
sum_prices = sum(price.price for price in prices)
average_price = sum_prices / len(prices)
return average_price

View File

@ -10,7 +10,7 @@ import aiohttp
import jinja2 import jinja2
from weasyprint import HTML from weasyprint import HTML
from app.services.base_service import BaseService from app.services.base_service import BaseService
from app.models.tcgplayer_products import TCGPlayerProduct from app.models.tcgplayer_products import TCGPlayerProduct, TCGPlayerGroup
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -20,6 +20,22 @@
</script> </script>
</head> </head>
<body class="bg-gray-900 min-h-screen text-gray-100"> <body class="bg-gray-900 min-h-screen text-gray-100">
<!-- Navigation Menu -->
<nav class="bg-gray-800 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<a href="/" class="text-xl font-bold text-gray-100">TCGPlayer Manager</a>
</div>
<div class="flex items-center space-x-4">
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Orders</a>
<a href="/manabox.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Manabox</a>
<a href="/transactions.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Transactions</a>
</div>
</div>
</div>
</nav>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8"> <div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
<h1 class="text-3xl font-bold text-gray-100 mb-2">TCGPlayer Order Management</h1> <h1 class="text-3xl font-bold text-gray-100 mb-2">TCGPlayer Order Management</h1>

86
app/static/manabox.html Normal file
View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manabox Inventory Management</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
}
}
}
</script>
</head>
<body class="bg-gray-900 min-h-screen text-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
<h1 class="text-3xl font-bold text-gray-100 mb-2">Manabox Inventory Management</h1>
<p class="text-gray-400">Upload and manage your Manabox inventory</p>
</div>
<!-- File Upload Section -->
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-100 mb-6">Upload Manabox CSV</h2>
<form id="uploadForm" class="space-y-4">
<div>
<label for="source" class="block text-sm font-medium text-gray-300 mb-2">Source</label>
<input type="text" id="source" name="source" required
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea id="description" name="description" rows="3" required
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<div>
<label for="csvFile" class="block text-sm font-medium text-gray-300 mb-2">CSV File</label>
<input type="file" id="csvFile" name="file" accept=".csv" required
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
</div>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Upload CSV
</button>
</form>
</div>
<!-- File Uploads List Section -->
<div class="bg-gray-800 rounded-xl shadow-sm p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-100">Recent Uploads</h2>
<div class="flex items-center space-x-4">
<button onclick="selectAllUploads()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Select All
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
<input type="checkbox" id="selectAll" class="rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Source</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Upload Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
</tr>
</thead>
<tbody id="uploadsList" class="divide-y divide-gray-700">
<!-- Uploads will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
<script src="/manabox.js"></script>
</body>
</html>

170
app/static/manabox.js Normal file
View File

@ -0,0 +1,170 @@
// API base URL
const API_BASE_URL = '/api';
// Selected uploads for actions
let selectedUploads = new Set();
// Show toast notification
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white ${
type === 'success' ? 'bg-green-600' : 'bg-red-600'
} transform translate-y-0 opacity-100 transition-all duration-300`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transform = 'translateY(100%)';
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Show loading state
function setLoading(isLoading) {
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
if (isLoading) {
button.disabled = true;
button.classList.add('opacity-50', 'cursor-not-allowed');
} else {
button.disabled = false;
button.classList.remove('opacity-50', 'cursor-not-allowed');
}
});
}
// Handle form submission
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('file', document.getElementById('csvFile').files[0]);
formData.append('source', document.getElementById('source').value);
formData.append('description', document.getElementById('description').value);
try {
setLoading(true);
const response = await fetch(`${API_BASE_URL}/manabox/process-csv`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to upload CSV');
}
showToast('CSV uploaded successfully');
document.getElementById('uploadForm').reset();
fetchUploads(); // Refresh the uploads list
} catch (error) {
showToast('Error uploading CSV: ' + error.message, 'error');
} finally {
setLoading(false);
}
});
// Fetch uploads from the API
async function fetchUploads() {
try {
setLoading(true);
const response = await fetch(`${API_BASE_URL}/manabox/manabox-file-uploads`);
if (!response.ok) {
throw new Error('Failed to fetch uploads');
}
const uploads = await response.json();
displayUploads(uploads);
} catch (error) {
showToast('Error fetching uploads: ' + error.message, 'error');
} finally {
setLoading(false);
}
}
// Display uploads in the UI
function displayUploads(uploads) {
const uploadsList = document.getElementById('uploadsList');
uploadsList.innerHTML = '';
if (!uploads || uploads.length === 0) {
uploadsList.innerHTML = '<tr><td colspan="5" class="px-6 py-4 text-center text-gray-400">No uploads found</td></tr>';
return;
}
uploads.forEach(upload => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-700';
row.dataset.uploadId = upload.id;
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap">
<input type="checkbox" class="upload-checkbox rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${upload.name || 'N/A'}</td>
<td class="px-6 py-4 text-sm text-gray-300">${upload.file_metadata?.description || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${formatDate(upload.created_at)}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs rounded-full bg-green-900/50 text-green-300">Processed</span>
</td>
`;
uploadsList.appendChild(row);
// Add click event listener to the checkbox
const checkbox = row.querySelector('.upload-checkbox');
checkbox.addEventListener('change', () => {
const uploadId = row.dataset.uploadId;
if (checkbox.checked) {
selectedUploads.add(uploadId);
} else {
selectedUploads.delete(uploadId);
}
});
});
}
// Helper function to format date
function formatDate(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// Select all uploads
function selectAllUploads() {
const checkboxes = document.querySelectorAll('.upload-checkbox');
const allSelected = checkboxes.length > 0 && Array.from(checkboxes).every(checkbox => checkbox.checked);
checkboxes.forEach(checkbox => {
checkbox.checked = !allSelected;
const row = checkbox.closest('tr');
const uploadId = row.dataset.uploadId;
if (!allSelected) {
selectedUploads.add(uploadId);
} else {
selectedUploads.delete(uploadId);
}
});
showToast(allSelected ? 'All uploads deselected' : 'All uploads selected');
}
// Initialize the page
document.addEventListener('DOMContentLoaded', () => {
fetchUploads();
// Add event listener for the select all checkbox
document.getElementById('selectAll').addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.upload-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
const row = checkbox.closest('tr');
const uploadId = row.dataset.uploadId;
if (e.target.checked) {
selectedUploads.add(uploadId);
} else {
selectedUploads.delete(uploadId);
}
});
});
});

View File

@ -105,4 +105,126 @@ button:hover {
select, button { select, button {
width: 100%; width: 100%;
} }
}
/* Transaction Page Styles */
.transaction-form {
max-width: 800px;
margin: 0 auto;
}
.transaction-form .form-group {
margin-bottom: 1.5rem;
}
.transaction-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.transaction-form .form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.transaction-form .btn-add {
margin-left: 0.5rem;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.transaction-form .items-section {
margin-top: 2rem;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.transaction-list {
margin-top: 2rem;
}
.transaction-card {
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
.transaction-card h3 {
margin-bottom: 0.5rem;
color: #333;
}
.transaction-card p {
margin-bottom: 0.25rem;
color: #666;
}
/* Modal Styles */
.modal-content {
border-radius: 8px;
}
.modal-header {
border-bottom: 1px solid #dee2e6;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #dee2e6;
padding: 1rem;
}
/* Dark Mode Support */
body.dark-mode {
background-color: #1a1a1a;
color: #ffffff;
}
body.dark-mode .container {
background-color: #2d2d2d;
}
body.dark-mode .transaction-card {
background-color: #2d2d2d;
border-color: #404040;
}
body.dark-mode .transaction-card h3 {
color: #ffffff;
}
body.dark-mode .transaction-card p {
color: #b3b3b3;
}
body.dark-mode .modal-content {
background-color: #2d2d2d;
color: #ffffff;
}
body.dark-mode .modal-header,
body.dark-mode .modal-footer {
border-color: #404040;
}
body.dark-mode .form-control {
background-color: #404040;
border-color: #505050;
color: #ffffff;
}
body.dark-mode .form-control:focus {
background-color: #404040;
border-color: #007bff;
color: #ffffff;
} }

View File

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transactions - AI Giga TCG</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
}
}
}
</script>
</head>
<body class="bg-gray-900 min-h-screen text-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
<h1 class="text-3xl font-bold text-gray-100 mb-2">Transactions</h1>
<p class="text-gray-400">Manage your transactions</p>
</div>
<!-- Create Transaction Button -->
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
<button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Create New Transaction
</button>
</div>
<!-- Transaction List -->
<div id="transactionList" class="bg-gray-800 rounded-xl shadow-sm p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-100">Recent Transactions</h2>
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-2">
<label for="limitSelect" class="text-sm text-gray-300">Show:</label>
<select id="limitSelect" class="rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
</div>
<div id="transactionsTable" class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Vendor/Customer</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Total</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Notes</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700" id="transactionsBody">
<!-- Transactions will be loaded here -->
</tbody>
</table>
</div>
<!-- Pagination Controls -->
<div class="flex justify-between items-center mt-4">
<button id="prevPageBtn" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
Previous
</button>
<span id="pageInfo" class="text-gray-300">Page 1</span>
<button id="nextPageBtn" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
Next
</button>
</div>
</div>
<!-- Create Transaction Modal -->
<div id="createTransactionModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center">
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-gray-100">Create Transaction</h3>
<button onclick="closeTransactionModal()" class="text-gray-400 hover:text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<form id="transactionForm" class="space-y-4">
<!-- Transaction Type -->
<div>
<label for="transactionType" class="block text-sm font-medium text-gray-300 mb-2">Transaction Type</label>
<select id="transactionType" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
<option value="purchase" selected>Purchase</option>
<option value="sale">Sale</option>
</select>
</div>
<!-- Vendor/Customer Selection -->
<div>
<div class="flex items-center justify-between mb-2">
<label for="vendorSelect" class="block text-sm font-medium text-gray-300">Vendor</label>
<button type="button" id="addVendorBtn" class="px-2 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Add New
</button>
</div>
<select id="vendorSelect" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
<option value="">Select a vendor</option>
</select>
</div>
<!-- Marketplace Selection (for sales) -->
<div id="marketplaceSection" class="hidden">
<div class="flex items-center justify-between mb-2">
<label for="marketplaceSelect" class="block text-sm font-medium text-gray-300">Marketplace</label>
<button type="button" id="addMarketplaceBtn" class="px-2 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Add New
</button>
</div>
<select id="marketplaceSelect" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
<option value="">Select a marketplace</option>
</select>
</div>
<!-- Transaction Date -->
<div>
<label for="transactionDate" class="block text-sm font-medium text-gray-300 mb-2">Transaction Date</label>
<input type="datetime-local" id="transactionDate" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" required>
</div>
<!-- Transaction Notes -->
<div>
<label for="transactionNotes" class="block text-sm font-medium text-gray-300 mb-2">Notes</label>
<textarea id="transactionNotes" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" rows="3"></textarea>
</div>
<!-- Items Section -->
<div id="itemsSection" class="border border-gray-700 rounded-lg p-4">
<h5 class="text-lg font-medium text-gray-100 mb-4">Items</h5>
<div id="itemsContainer" class="space-y-4">
<!-- Items will be added here -->
</div>
<button type="button" id="addItemBtn" class="mt-4 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors">
Add Item
</button>
</div>
</form>
<div class="flex justify-end space-x-3 mt-6">
<button onclick="closeTransactionModal()" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors">
Cancel
</button>
<button id="saveTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Save Transaction
</button>
</div>
</div>
</div>
<!-- Transaction Details Modal -->
<div id="transactionDetailsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center">
<div class="bg-gray-800 rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-gray-100">Transaction Details</h3>
<button onclick="closeTransactionDetailsModal()" class="text-gray-400 hover:text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div id="transactionDetails" class="space-y-4">
<!-- Transaction details will be loaded here -->
</div>
</div>
</div>
</div>
<script src="transactions.js"></script>
</body>
</html>

1077
app/static/transactions.js Normal file

File diff suppressed because it is too large Load Diff