@@ -24,11 +24,18 @@ Chronicle.register('shop_inventory', {
2424 var editable = el . dataset . editable === 'true' ;
2525 var csrfToken = el . dataset . csrfToken || '' ;
2626
27+ // Transaction endpoints derive from the campaign URL.
28+ var txEndpoint = campaignUrl + '/armory/transactions' ;
29+ var purchaseEndpoint = campaignUrl + '/armory/purchase' ;
30+
2731 // Internal state.
2832 var state = {
2933 items : [ ] ,
3034 loading : true ,
3135 addMode : false ,
36+ showTransactions : false ,
37+ transactions : [ ] ,
38+ txLoading : false ,
3239 searchQuery : '' ,
3340 searchResults : [ ] ,
3441 searchTimer : null ,
@@ -91,6 +98,26 @@ Chronicle.register('shop_inventory', {
9198 '.shop-inv-action-btn.blue { background: #3b82f6; }' ,
9299 '.shop-inv-action-btn.blue:hover:not(:disabled) { background: #2563eb; }' ,
93100 '.shop-inv-label { font-size: 0.6875rem; color: #6b7280; font-weight: 500; }' ,
101+ '.shop-inv-tx-toggle { font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 0.25rem; background: #f3f4f6; color: #6b7280; border: 1px solid #e5e7eb; cursor: pointer; margin-left: 0.375rem; }' ,
102+ '.dark .shop-inv-tx-toggle { background: #374151; color: #9ca3af; border-color: #4b5563; }' ,
103+ '.shop-inv-tx-toggle:hover { background: #e5e7eb; }' ,
104+ '.dark .shop-inv-tx-toggle:hover { background: #4b5563; }' ,
105+ '.shop-inv-tx-section { margin-top: 0.75rem; border-top: 1px solid #e5e7eb; padding-top: 0.75rem; }' ,
106+ '.dark .shop-inv-tx-section { border-color: #374151; }' ,
107+ '.shop-inv-tx-title { font-size: 0.75rem; font-weight: 600; color: #6b7280; margin-bottom: 0.5rem; }' ,
108+ '.shop-inv-tx-list { display: flex; flex-direction: column; gap: 0.25rem; }' ,
109+ '.shop-inv-tx-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0.375rem; font-size: 0.75rem; border-radius: 0.25rem; }' ,
110+ '.shop-inv-tx-row:nth-child(even) { background: #f9fafb; }' ,
111+ '.dark .shop-inv-tx-row:nth-child(even) { background: #1f2937; }' ,
112+ '.shop-inv-tx-type { font-weight: 500; text-transform: capitalize; min-width: 4rem; }' ,
113+ '.shop-inv-tx-type.purchase { color: #059669; }' ,
114+ '.shop-inv-tx-type.sale { color: #2563eb; }' ,
115+ '.shop-inv-tx-type.gift { color: #8b5cf6; }' ,
116+ '.shop-inv-tx-type.restock { color: #d97706; }' ,
117+ '.shop-inv-tx-detail { flex: 1; color: #4b5563; }' ,
118+ '.dark .shop-inv-tx-detail { color: #9ca3af; }' ,
119+ '.shop-inv-tx-price { font-weight: 500; color: #d97706; white-space: nowrap; }' ,
120+ '.shop-inv-tx-date { color: #9ca3af; font-size: 0.6875rem; white-space: nowrap; }' ,
94121 ] . join ( '\n' ) ;
95122 el . appendChild ( style ) ;
96123
@@ -152,9 +179,101 @@ Chronicle.register('shop_inventory', {
152179 wrapper . appendChild ( list ) ;
153180 }
154181
182+ // Transaction history section.
183+ if ( editable ) {
184+ var txToggle = document . createElement ( 'button' ) ;
185+ txToggle . className = 'shop-inv-tx-toggle' ;
186+ txToggle . innerHTML = '<i class="fas fa-receipt"></i> ' + ( state . showTransactions ? 'Hide Transactions' : 'Transactions' ) ;
187+ txToggle . onclick = function ( ) {
188+ state . showTransactions = ! state . showTransactions ;
189+ if ( state . showTransactions && state . transactions . length === 0 ) {
190+ loadTransactions ( ) ;
191+ }
192+ render ( ) ;
193+ } ;
194+ header . appendChild ( txToggle ) ;
195+ }
196+
197+ if ( state . showTransactions ) {
198+ wrapper . appendChild ( renderTransactionSection ( ) ) ;
199+ }
200+
155201 el . appendChild ( wrapper ) ;
156202 }
157203
204+ function renderTransactionSection ( ) {
205+ var section = document . createElement ( 'div' ) ;
206+ section . className = 'shop-inv-tx-section' ;
207+
208+ var title = document . createElement ( 'div' ) ;
209+ title . className = 'shop-inv-tx-title' ;
210+ title . textContent = 'Recent Transactions' ;
211+ section . appendChild ( title ) ;
212+
213+ if ( state . txLoading ) {
214+ var loading = document . createElement ( 'div' ) ;
215+ loading . className = 'shop-inv-empty' ;
216+ loading . textContent = 'Loading transactions...' ;
217+ section . appendChild ( loading ) ;
218+ } else if ( state . transactions . length === 0 ) {
219+ var empty = document . createElement ( 'div' ) ;
220+ empty . className = 'shop-inv-empty' ;
221+ empty . textContent = 'No transactions recorded.' ;
222+ section . appendChild ( empty ) ;
223+ } else {
224+ var list = document . createElement ( 'div' ) ;
225+ list . className = 'shop-inv-tx-list' ;
226+ for ( var i = 0 ; i < state . transactions . length ; i ++ ) {
227+ list . appendChild ( renderTransactionRow ( state . transactions [ i ] ) ) ;
228+ }
229+ section . appendChild ( list ) ;
230+ }
231+ return section ;
232+ }
233+
234+ function renderTransactionRow ( tx ) {
235+ var row = document . createElement ( 'div' ) ;
236+ row . className = 'shop-inv-tx-row' ;
237+
238+ var typeSpan = document . createElement ( 'span' ) ;
239+ typeSpan . className = 'shop-inv-tx-type ' + ( tx . transaction_type || '' ) ;
240+ typeSpan . textContent = tx . transaction_type || 'unknown' ;
241+ row . appendChild ( typeSpan ) ;
242+
243+ var detail = document . createElement ( 'span' ) ;
244+ detail . className = 'shop-inv-tx-detail' ;
245+ var parts = [ ] ;
246+ if ( tx . item_name ) parts . push ( tx . item_name ) ;
247+ if ( tx . quantity > 1 ) parts . push ( 'x' + tx . quantity ) ;
248+ if ( tx . buyer_name ) parts . push ( '\u2192 ' + tx . buyer_name ) ;
249+ detail . textContent = parts . join ( ' ' ) ;
250+ row . appendChild ( detail ) ;
251+
252+ if ( tx . price_paid ) {
253+ var price = document . createElement ( 'span' ) ;
254+ price . className = 'shop-inv-tx-price' ;
255+ price . textContent = tx . price_paid ;
256+ row . appendChild ( price ) ;
257+ }
258+
259+ var date = document . createElement ( 'span' ) ;
260+ date . className = 'shop-inv-tx-date' ;
261+ date . textContent = formatTxDate ( tx . created_at ) ;
262+ row . appendChild ( date ) ;
263+
264+ return row ;
265+ }
266+
267+ function formatTxDate ( isoStr ) {
268+ if ( ! isoStr ) return '' ;
269+ try {
270+ var d = new Date ( isoStr ) ;
271+ return d . toLocaleDateString ( undefined , { month : 'short' , day : 'numeric' } ) ;
272+ } catch ( e ) {
273+ return '' ;
274+ }
275+ }
276+
158277 function renderItem ( item ) {
159278 var meta = item . metadata || { } ;
160279 var isCustom = ! item . targetEntityID ;
@@ -577,6 +696,35 @@ Chronicle.register('shop_inventory', {
577696 } ) ;
578697 }
579698
699+ // Load transaction history for this shop entity.
700+ function loadTransactions ( ) {
701+ // Extract entity ID from the relations endpoint.
702+ // Format: /campaigns/:id/entities/:eid/relations
703+ var parts = relationsEndpoint . split ( '/' ) ;
704+ var eidIdx = parts . indexOf ( 'entities' ) ;
705+ var entityId = eidIdx >= 0 ? parts [ eidIdx + 1 ] : '' ;
706+ if ( ! entityId ) return ;
707+
708+ state . txLoading = true ;
709+ render ( ) ;
710+
711+ Chronicle . apiFetch ( txEndpoint + '?shop=' + encodeURIComponent ( entityId ) + '&per_page=20' , { method : 'GET' } )
712+ . then ( function ( res ) {
713+ if ( ! res . ok ) throw new Error ( 'Failed to load transactions' ) ;
714+ return res . json ( ) ;
715+ } )
716+ . then ( function ( data ) {
717+ state . transactions = data . data || [ ] ;
718+ state . txLoading = false ;
719+ render ( ) ;
720+ } )
721+ . catch ( function ( err ) {
722+ console . error ( 'Shop inventory: failed to load transactions' , err ) ;
723+ state . txLoading = false ;
724+ render ( ) ;
725+ } ) ;
726+ }
727+
580728 // Initial load.
581729 loadInventory ( ) ;
582730 loadEntityTypes ( ) ;
0 commit comments