diff --git a/.commitmessage b/.commitmessage
index 07f235e..5580a49 100644
--- a/.commitmessage
+++ b/.commitmessage
@@ -1,4 +1,4 @@
-feat: restrict admin areas and show mobile pickups
+fix: show membership date for stores without pickups
-Limit monitoring and journal access to admins, reduce regular-user navigation,
-and show the latest pickup directly in mobile configuration cards.
+Treat Foodsharing's zero lastFetch value as no pickup, store memberSince, and
+show compact, visible mobile status information without hover tooltips.
diff --git a/services/lastPickupInfo.js b/services/lastPickupInfo.js
index d19a9bf..e90977d 100644
--- a/services/lastPickupInfo.js
+++ b/services/lastPickupInfo.js
@@ -25,16 +25,23 @@ function getStaleLastPickupStoreIds(config = [], now = Date.now()) {
function parseLastFetchTimestamp(value) {
if (typeof value === 'string') {
- return parseTimestamp(value);
+ if (value.trim() !== '' && Number.isFinite(Number(value))) {
+ return parseLastFetchTimestamp(Number(value));
+ }
+ const timestamp = parseTimestamp(value);
+ return timestamp && timestamp > 0 ? timestamp : null;
}
if (!Number.isFinite(Number(value))) {
return null;
}
const numericValue = Number(value);
+ if (numericValue <= 0) {
+ return null;
+ }
return numericValue > 1e12 ? numericValue : numericValue * 1000;
}
-function updateLastPickupInfo(entry, lastFetchValue, checkedAt = new Date()) {
+function updateLastPickupInfo(entry, lastFetchValue, memberSinceValue, checkedAt = new Date()) {
if (!entry || typeof entry !== 'object') {
return false;
}
@@ -51,6 +58,17 @@ function updateLastPickupInfo(entry, lastFetchValue, checkedAt = new Date()) {
entry.lastPickupAt = lastPickupAt;
changed = true;
}
+ } else if (entry.lastPickupAt) {
+ delete entry.lastPickupAt;
+ changed = true;
+ }
+ const memberSinceTimestamp = parseTimestamp(memberSinceValue);
+ if (memberSinceTimestamp !== null && memberSinceTimestamp > 0) {
+ const memberSinceAt = new Date(memberSinceTimestamp).toISOString();
+ if (entry.memberSinceAt !== memberSinceAt) {
+ entry.memberSinceAt = memberSinceAt;
+ changed = true;
+ }
}
return changed;
}
diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js
index 4aaa3a2..82a052a 100644
--- a/services/pickupScheduler.js
+++ b/services/pickupScheduler.js
@@ -1515,7 +1515,8 @@ async function checkDormantMembers(sessionId, options = {}) {
const lastFetchRaw = memberEntry.lastFetch ?? memberEntry.last_fetch ?? null;
const lastFetchMs = parseLastFetchTimestamp(lastFetchRaw);
const configEntry = configEntryMap.get(storeId)?.entry;
- if (updateLastPickupInfo(configEntry, lastFetchRaw)) {
+ const memberSinceRaw = memberEntry.memberSince ?? memberEntry.member_since ?? null;
+ if (updateLastPickupInfo(configEntry, lastFetchRaw, memberSinceRaw)) {
configChanged = true;
}
if (sendWarnings && !skipMap.get(storeId) && (!lastFetchMs || lastFetchMs < fourMonthsAgo)) {
diff --git a/src/components/DashboardView.js b/src/components/DashboardView.js
index 5bec4de..a543d15 100644
--- a/src/components/DashboardView.js
+++ b/src/components/DashboardView.js
@@ -103,23 +103,17 @@ const ColumnSelectFilter = ({ column, options, placeholder = 'Alle' }) => {
};
const formatLastPickupLabel = (value) => {
- if (!value) {
- return 'Unbekannt';
- }
const date = new Date(value);
- if (Number.isNaN(date.getTime())) {
- return 'Unbekannt';
+ if (!value || Number.isNaN(date.getTime()) || date.getTime() <= 0) {
+ return null;
}
return date.toLocaleDateString('de-DE');
};
const formatLastPickupRelative = (value) => {
- if (!value) {
- return 'Unbekannt';
- }
const date = new Date(value);
- if (Number.isNaN(date.getTime())) {
- return 'Unbekannt';
+ if (!value || Number.isNaN(date.getTime()) || date.getTime() <= 0) {
+ return null;
}
return formatDistanceToNowStrict(date, { addSuffix: true, locale: de });
};
@@ -133,6 +127,23 @@ const getLastPickupTimestamp = (value) => {
return Number.isNaN(time) ? -1 : time;
};
+const formatMemberSinceLabel = (value) => {
+ const date = new Date(value);
+ if (!value || Number.isNaN(date.getTime()) || date.getTime() <= 0) {
+ return null;
+ }
+ return date.toLocaleDateString('de-DE');
+};
+
+const getLastPickupDescription = (entry) => {
+ const lastPickupLabel = formatLastPickupLabel(entry.lastPickupAt);
+ if (lastPickupLabel) {
+ return `Letzte Abholung: ${lastPickupLabel}`;
+ }
+ const memberSinceLabel = formatMemberSinceLabel(entry.memberSinceAt);
+ return memberSinceLabel ? `Noch nicht abgeholt. Beitritt: ${memberSinceLabel}` : 'Noch nicht abgeholt';
+};
+
const getLastPickupStyle = (value) => {
const time = getLastPickupTimestamp(value);
if (time <= 0) {
@@ -496,13 +507,16 @@ const DashboardView = ({
cell: ({ row }) => {
const lastPickupAt = row.original.lastPickupAt;
const style = getLastPickupStyle(lastPickupAt);
+ const relative = formatLastPickupRelative(lastPickupAt);
+ const memberSince = formatMemberSinceLabel(row.original.memberSinceAt);
return (
- {formatLastPickupRelative(lastPickupAt)}
+ {relative || 'Noch nicht abgeholt'}
+ {!relative && memberSince && Beitritt: {memberSince}}
);
},
@@ -520,9 +534,7 @@ const DashboardView = ({
{
- const date = new Date(value);
- if (!value || Number.isNaN(date.getTime())) {
- return { relative: 'Unbekannt', exact: null };
+const formatDate = (date) => date.toLocaleDateString('de-DE', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+});
+
+const formatLastPickup = (lastPickupAt, memberSinceAt) => {
+ const date = new Date(lastPickupAt);
+ if (!lastPickupAt || Number.isNaN(date.getTime()) || date.getTime() <= 0) {
+ const memberSince = new Date(memberSinceAt);
+ return {
+ state: 'never',
+ memberSince: memberSinceAt && !Number.isNaN(memberSince.getTime())
+ ? formatDate(memberSince)
+ : null
+ };
}
+ const ageDays = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24);
return {
+ state: ageDays >= 90 ? 'stale' : 'current',
relative: formatDistanceToNowStrict(date, { addSuffix: true, locale: de }),
- exact: date.toLocaleDateString('de-DE')
+ exact: formatDate(date)
};
};
@@ -97,7 +111,7 @@ const MobileConfigList = ({
{filteredEntries.map((entry) => {
const range = entry.desiredDateRange || (entry.desiredDate ? { start: entry.desiredDate, end: entry.desiredDate } : null);
const slots = formatRegularPickup(regularPickupMap?.[entry.id]);
- const lastPickup = formatLastPickup(entry.lastPickupAt);
+ const lastPickup = formatLastPickup(entry.lastPickupAt, entry.memberSinceAt);
return (
{entry.label || `Store ${entry.id}`}
#{entry.id}
{slots && Slots: {slots}
}
-
- Letzte Abholung: {lastPickup.relative}
+
+ {lastPickup.state === 'never' ? (
+ <>
+ Noch nicht abgeholt{lastPickup.memberSince && <> · Beitritt: {lastPickup.memberSince}>}
+ >
+ ) : (
+ <>
+ Letzte Abholung: {lastPickup.relative} · {lastPickup.exact}
+ >
+ )}
{
expect(screen.getAllByText(/Letzte Abholung:/)[0]).toBeInTheDocument();
});
+ it('shows a visible membership date when no pickup has happened yet', () => {
+ render();
+
+ expect(screen.getByText(/Noch nicht abgeholt/)).toBeInTheDocument();
+ expect(screen.getByText('01.07.2026')).toBeInTheDocument();
+ });
+
+ it('marks stale pickups in the mobile card', () => {
+ render();
+
+ expect(document.querySelector('.mobile-config-card__last-pickup--stale')).toBeInTheDocument();
+ });
+
it('uses the existing configuration callbacks', () => {
render();
diff --git a/src/utils/lastPickupInfo.test.js b/src/utils/lastPickupInfo.test.js
index f1b0ce8..bfded25 100644
--- a/src/utils/lastPickupInfo.test.js
+++ b/src/utils/lastPickupInfo.test.js
@@ -21,11 +21,23 @@ describe('last pickup info', () => {
it('records a successful check independently from the pickup date', () => {
const entry = { id: '33875', lastPickupAt: '2026-01-01T00:00:00.000Z' };
- expect(updateLastPickupInfo(entry, 1785945600, new Date('2026-08-05T12:00:00.000Z'))).toBe(true);
+ expect(updateLastPickupInfo(entry, 1785945600, '2024-05-01T00:00:00.000Z', new Date('2026-08-05T12:00:00.000Z'))).toBe(true);
expect(entry).toEqual({
id: '33875',
lastPickupAt: '2026-08-05T16:00:00.000Z',
- lastPickupCheckedAt: '2026-08-05T12:00:00.000Z'
+ lastPickupCheckedAt: '2026-08-05T12:00:00.000Z',
+ memberSinceAt: '2024-05-01T00:00:00.000Z'
+ });
+ });
+
+ it('treats a zero last-fetch value as no pickup and records the membership date', () => {
+ const entry = { id: '69375', lastPickupAt: '1970-01-01T00:00:00.000Z' };
+
+ expect(updateLastPickupInfo(entry, 0, '2025-07-12T00:00:00.000Z', new Date('2026-08-05T12:00:00.000Z'))).toBe(true);
+ expect(entry).toEqual({
+ id: '69375',
+ lastPickupCheckedAt: '2026-08-05T12:00:00.000Z',
+ memberSinceAt: '2025-07-12T00:00:00.000Z'
});
});
@@ -33,5 +45,7 @@ describe('last pickup info', () => {
expect(parseLastFetchTimestamp(1785945600)).toBe(1785945600000);
expect(parseLastFetchTimestamp(1785945600000)).toBe(1785945600000);
expect(parseLastFetchTimestamp('2026-08-05T00:00:00.000Z')).toBe(1785888000000);
+ expect(parseLastFetchTimestamp(0)).toBeNull();
+ expect(parseLastFetchTimestamp('0')).toBeNull();
});
});