Skip to content

Commit a787358

Browse files
committed
feat: add Shops tab to Sync Dashboard for shop discovery
New "Shops" tab in the dashboard lists all shop-type entities from Chronicle with one-click "Open" buttons. Each row shows: - Shop name with entity type icon and color - Shop subtype (General Store, Blacksmith, etc.) - Shopkeeper name - Sync status and privacy badge Clicking "Open" launches the ShopWindow for that entity via the existing ShopWidget module, removing the need to find and right-click the linked journal entry. https://claude.ai/code/session_01XMwxFR8BCi5XvgaSVMSBZB
1 parent fb2c431 commit a787358

4 files changed

Lines changed: 252 additions & 0 deletions

File tree

foundry-module/lang/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"NotConfiguredHint": "Go to Module Settings to enter your Chronicle URL, API key, and Campaign ID.",
5353
"Tabs": {
5454
"Entities": "Entities",
55+
"Shops": "Shops",
5556
"Maps": "Maps",
5657
"Calendar": "Calendar",
5758
"Status": "Status"
@@ -85,6 +86,14 @@
8586
"Unlink": "Unlink",
8687
"NoMaps": "No maps found in this Chronicle campaign."
8788
},
89+
"Shops": {
90+
"Open": "Open",
91+
"Synced": "Synced",
92+
"NoShops": "No shops found in this campaign.",
93+
"NoShopsHint": "Create shop entities in Chronicle to see them here.",
94+
"NoShopType": "No shop entity type found.",
95+
"NoShopTypeHint": "Add a \"Shop\" entity type to your campaign in Chronicle."
96+
},
8897
"Calendar": {
8998
"CalendarModule": "Calendar Module",
9099
"Chronicle": "Chronicle",

foundry-module/scripts/sync-dashboard.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
5555
reconnect: SyncDashboard.#onReconnectAction,
5656
'clear-log': SyncDashboard.#onClearLogAction,
5757
'open-settings': SyncDashboard.#onOpenSettingsAction,
58+
'open-shop': SyncDashboard.#onOpenShopAction,
5859
},
5960
};
6061

@@ -129,6 +130,14 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
129130
console.error('Chronicle Dashboard: Failed to load maps', err);
130131
}
131132

133+
// Build shops tab data.
134+
let shopData = { shops: [] };
135+
try {
136+
shopData = await this._buildShopData();
137+
} catch (err) {
138+
console.error('Chronicle Dashboard: Failed to load shops', err);
139+
}
140+
132141
// Build calendar tab data.
133142
let calendarData = { available: false };
134143
try {
@@ -152,6 +161,9 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
152161
hasChronicleOnly: entityGroups.some(g => g.entities.some(e => e.status === 'chronicle-only')),
153162
hasFoundryOnly: foundryOnlyJournals.length > 0,
154163

164+
// Shops tab.
165+
...shopData,
166+
155167
// Maps tab.
156168
...mapData,
157169

@@ -350,6 +362,72 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
350362
};
351363
}
352364

365+
// ---------------------------------------------------------------------------
366+
// Shop data
367+
// ---------------------------------------------------------------------------
368+
369+
/**
370+
* Build shops tab data: all shop-type entities from Chronicle with
371+
* inventory counts and sync status.
372+
* @returns {Promise<object>}
373+
* @private
374+
*/
375+
async _buildShopData() {
376+
// Ensure entity types are cached.
377+
if (!this._cache.entityTypes) {
378+
this._cache.entityTypes = await this.api.get('/entity-types');
379+
}
380+
const types = this._cache.entityTypes || [];
381+
382+
// Find the shop entity type by slug or name.
383+
const shopType = types.find(t =>
384+
t.slug === 'shop' || t.name?.toLowerCase() === 'shop'
385+
);
386+
387+
if (!shopType) {
388+
return { shops: [], shopTypeExists: false };
389+
}
390+
391+
// Ensure entities are cached.
392+
if (!this._cache.entities) {
393+
// Trigger entity cache population via _buildEntityGroups path.
394+
await this._buildEntityGroups(this._getExclusions());
395+
}
396+
const allEntities = this._cache.entities || [];
397+
398+
// Filter to shop entities.
399+
const shopEntities = allEntities.filter(e => e.entity_type_id === shopType.id);
400+
401+
// Index Foundry journals by entityId flag.
402+
const journalsByEntityId = new Map();
403+
for (const j of game.journal.contents) {
404+
const eid = j.getFlag(FLAG_SCOPE, 'entityId');
405+
if (eid) journalsByEntityId.set(eid, j);
406+
}
407+
408+
const shops = shopEntities.map(entity => {
409+
const journal = journalsByEntityId.get(entity.id);
410+
const fields = entity.fields_data || {};
411+
412+
return {
413+
id: entity.id,
414+
name: entity.name,
415+
shopType: fields.shop_type || '',
416+
shopKeeper: fields.shop_keeper || '',
417+
synced: !!journal,
418+
journalId: journal?.id ?? null,
419+
isPrivate: entity.is_private ?? false,
420+
};
421+
});
422+
423+
return {
424+
shops,
425+
shopTypeExists: true,
426+
shopTypeIcon: shopType.icon || 'fa-store',
427+
shopTypeColor: shopType.color || '#f97316',
428+
};
429+
}
430+
353431
// ---------------------------------------------------------------------------
354432
// Calendar data
355433
// ---------------------------------------------------------------------------
@@ -707,6 +785,18 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
707785
game.settings.sheet.render(true);
708786
}
709787

788+
/** Open a shop window via the ShopWidget module. */
789+
static #onOpenShopAction(event, target) {
790+
const entityId = target.dataset.entityId;
791+
const shopName = target.dataset.shopName || 'Shop';
792+
const shopWidget = this._syncManager?._modules?.find(
793+
m => m.constructor?.name === 'ShopWidget'
794+
);
795+
if (shopWidget) {
796+
shopWidget.openShop(entityId, shopName);
797+
}
798+
}
799+
710800
// ---------------------------------------------------------------------------
711801
// Actions (business logic)
712802
// ---------------------------------------------------------------------------

foundry-module/styles/chronicle-sync.css

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,98 @@
506506
color: inherit;
507507
}
508508

509+
/* ================================================================= */
510+
/* SHOPS TAB */
511+
/* ================================================================= */
512+
513+
.shop-list {
514+
padding: 4px 0;
515+
}
516+
517+
.shop-row {
518+
display: flex;
519+
align-items: center;
520+
justify-content: space-between;
521+
padding: 8px 10px;
522+
border-radius: 4px;
523+
margin-bottom: 2px;
524+
transition: background 0.1s;
525+
}
526+
527+
.shop-row:hover {
528+
background: rgba(255, 255, 255, 0.03);
529+
}
530+
531+
.shop-row.synced {
532+
background: rgba(74, 222, 128, 0.04);
533+
}
534+
535+
.shop-info {
536+
display: flex;
537+
align-items: center;
538+
gap: 10px;
539+
flex: 1;
540+
min-width: 0;
541+
}
542+
543+
.shop-row-icon {
544+
width: 36px;
545+
height: 36px;
546+
border-radius: 6px;
547+
display: flex;
548+
align-items: center;
549+
justify-content: center;
550+
font-size: 16px;
551+
flex-shrink: 0;
552+
}
553+
554+
.shop-details {
555+
display: flex;
556+
flex-direction: column;
557+
gap: 1px;
558+
min-width: 0;
559+
}
560+
561+
.shop-name {
562+
font-size: 13px;
563+
font-weight: 500;
564+
overflow: hidden;
565+
text-overflow: ellipsis;
566+
white-space: nowrap;
567+
}
568+
569+
.shop-subtype {
570+
font-size: 11px;
571+
color: rgba(255, 255, 255, 0.4);
572+
}
573+
574+
.shop-keeper {
575+
font-size: 11px;
576+
color: rgba(255, 255, 255, 0.35);
577+
}
578+
579+
.shop-keeper i {
580+
font-size: 10px;
581+
margin-right: 2px;
582+
}
583+
584+
.shop-actions {
585+
display: flex;
586+
align-items: center;
587+
gap: 6px;
588+
flex-shrink: 0;
589+
}
590+
591+
.shop-private-badge {
592+
color: rgba(255, 255, 255, 0.3);
593+
font-size: 12px;
594+
}
595+
596+
.shop-synced-badge {
597+
font-size: 11px;
598+
color: #4ade80;
599+
}
600+
509601
/* ================================================================= */
510602
/* CALENDAR TAB */
511603
/* ================================================================= */

foundry-module/templates/sync-dashboard.hbs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
<a class="item" data-tab="entities">
1818
<i class="fa-solid fa-book"></i> Entities
1919
</a>
20+
<a class="item" data-tab="shops">
21+
<i class="fa-solid fa-store"></i> Shops
22+
</a>
2023
<a class="item" data-tab="maps">
2124
<i class="fa-solid fa-map"></i> Maps
2225
</a>
@@ -151,6 +154,64 @@
151154
</div>
152155
</div>
153156

157+
{{!-- ================================================================= --}}
158+
{{!-- SHOPS TAB --}}
159+
{{!-- ================================================================= --}}
160+
<div class="tab" data-tab="shops" data-group="primary">
161+
{{#if shops.length}}
162+
<div class="shop-list">
163+
{{#each shops}}
164+
<div class="shop-row {{#if synced}}synced{{/if}}">
165+
<div class="shop-info">
166+
<span class="shop-row-icon" style="background-color: {{../shopTypeColor}}20; color: {{../shopTypeColor}}">
167+
<i class="fa-solid {{../shopTypeIcon}}"></i>
168+
</span>
169+
<div class="shop-details">
170+
<span class="shop-name">{{name}}</span>
171+
{{#if shopType}}
172+
<span class="shop-subtype">{{shopType}}</span>
173+
{{/if}}
174+
{{#if shopKeeper}}
175+
<span class="shop-keeper"><i class="fa-solid fa-user"></i> {{shopKeeper}}</span>
176+
{{/if}}
177+
</div>
178+
</div>
179+
<div class="shop-actions">
180+
{{#if isPrivate}}
181+
<span class="shop-private-badge" title="Private (DM only)">
182+
<i class="fa-solid fa-lock"></i>
183+
</span>
184+
{{/if}}
185+
{{#if synced}}
186+
<span class="shop-synced-badge">
187+
<i class="fa-solid fa-check"></i> Synced
188+
</span>
189+
{{/if}}
190+
<button type="button" class="dashboard-btn btn-sm"
191+
data-action="open-shop"
192+
data-entity-id="{{id}}"
193+
data-shop-name="{{name}}">
194+
<i class="fa-solid fa-door-open"></i> Open
195+
</button>
196+
</div>
197+
</div>
198+
{{/each}}
199+
</div>
200+
{{else}}
201+
<div class="dashboard-empty-tab">
202+
{{#if shopTypeExists}}
203+
<i class="fa-solid fa-store fa-2x"></i>
204+
<p>No shops found in this campaign.</p>
205+
<p class="hint">Create shop entities in Chronicle to see them here.</p>
206+
{{else}}
207+
<i class="fa-solid fa-store fa-2x"></i>
208+
<p>No shop entity type found.</p>
209+
<p class="hint">Add a "Shop" entity type to your campaign in Chronicle.</p>
210+
{{/if}}
211+
</div>
212+
{{/if}}
213+
</div>
214+
154215
{{!-- ================================================================= --}}
155216
{{!-- MAPS TAB --}}
156217
{{!-- ================================================================= --}}

0 commit comments

Comments
 (0)