const BASE = '/api'; function getToken() { return localStorage.getItem('tc_token') || sessionStorage.getItem('tc_token'); } // SQLite datetime('now') returns "YYYY-MM-DD HH:MM:SS" with no timezone marker. // Browsers parse bare strings like this as LOCAL time, but the value is actually UTC. // Appending 'Z' forces correct UTC interpretation so local display is always right. export function parseTS(ts) { if (!ts) return new Date(NaN); // Already has timezone info (contains T and Z/+ or ends in Z) — leave alone if (/Z$|[+-]\d{2}:\d{2}$/.test(ts) || (ts.includes('T') && ts.includes('Z'))) return new Date(ts); // Replace the space separator SQLite uses and append Z return new Date(ts.replace(' ', 'T') + 'Z'); } async function req(method, path, body, opts = {}) { const token = getToken(); const headers = {}; if (token) headers['Authorization'] = `Bearer ${token}`; let fetchOpts = { method, headers }; if (body instanceof FormData) { fetchOpts.body = body; } else if (body) { headers['Content-Type'] = 'application/json'; fetchOpts.body = JSON.stringify(body); } const res = await fetch(BASE + path, fetchOpts); const data = await res.json(); if (!res.ok) { // Session displaced by a new login elsewhere — force logout if (res.status === 401 && data.error?.includes('Session expired')) { localStorage.removeItem('tc_token'); sessionStorage.removeItem('tc_token'); window.dispatchEvent(new CustomEvent('rosterchirp:session-displaced')); } throw new Error(data.error || 'Request failed'); } return data; } export const api = { // Auth login: (body) => req('POST', '/auth/login', body), submitSupport: (body) => req('POST', '/auth/support', body), logout: () => req('POST', '/auth/logout'), me: () => req('GET', '/auth/me'), changePassword: (body) => req('POST', '/auth/change-password', body), // Users getUsers: () => req('GET', '/users'), searchUsers: (q, groupId) => req('GET', `/users/search?q=${encodeURIComponent(q)}${groupId ? `&groupId=${groupId}` : ''}`), createUser: (body) => req('POST', '/users', body), updateUser: (id, body) => req('PATCH', `/users/${id}`, body), bulkUsers: (users) => req('POST', '/users/bulk', { users }), updateName: (id, name) => req('PATCH', `/users/${id}/name`, { name }), updateRole: (id, role) => req('PATCH', `/users/${id}/role`, { role }), resetPassword: (id, password) => req('PATCH', `/users/${id}/reset-password`, { password }), suspendUser: (id) => req('PATCH', `/users/${id}/suspend`), activateUser: (id) => req('PATCH', `/users/${id}/activate`), deleteUser: (id) => req('DELETE', `/users/${id}`), checkDisplayName: (name) => req('GET', `/users/check-display-name?name=${encodeURIComponent(name)}`), updateProfile: (body) => req('PATCH', '/users/me/profile', body), // body: { displayName, aboutMe, hideAdminTag, allowDm } uploadAvatar: (file) => { const form = new FormData(); form.append('avatar', file); return req('POST', '/users/me/avatar', form); }, // Groups getGroups: () => req('GET', '/groups'), createGroup: (body) => req('POST', '/groups', body), renameGroup: (id, name) => req('PATCH', `/groups/${id}/rename`, { name }), setCustomGroupName: (id, name) => req('PATCH', `/groups/${id}/custom-name`, { name }), getHelp: () => req('GET', '/help'), getHelpStatus: () => req('GET', '/help/status'), dismissHelp: (dismissed) => req('POST', '/help/dismiss', { dismissed }), getMembers: (id) => req('GET', `/groups/${id}/members`), addMember: (groupId, userId) => req('POST', `/groups/${groupId}/members`, { userId }), removeMember: (groupId, userId) => req('DELETE', `/groups/${groupId}/members/${userId}`), leaveGroup: (id) => req('DELETE', `/groups/${id}/leave`), takeOwnership: (id) => req('POST', `/groups/${id}/take-ownership`), deleteGroup: (id) => req('DELETE', `/groups/${id}`), // Messages getMessages: (groupId, before) => req('GET', `/messages/group/${groupId}${before ? `?before=${before}` : ''}`), sendMessage: (groupId, body) => req('POST', `/messages/group/${groupId}`, body), uploadImage: (groupId, file, extra = {}) => { const form = new FormData(); form.append('image', file); if (extra.replyToId) form.append('replyToId', extra.replyToId); if (extra.content) form.append('content', extra.content); return req('POST', `/messages/group/${groupId}/image`, form); }, deleteMessage: (id) => req('DELETE', `/messages/${id}`), toggleReaction: (id, emoji) => req('POST', `/messages/${id}/reactions`, { emoji }), // Settings getSettings: () => req('GET', '/settings'), updateAppName: (name) => req('PATCH', '/settings/app-name', { name }), updateColors: (body) => req('PATCH', '/settings/colors', body), registerCode: (code) => req('POST', '/settings/register', { code }), updateTeamSettings: (body) => req('PATCH', '/settings/team', body), // Schedule Manager getMyScheduleGroups: () => req('GET', '/schedule/my-groups'), getEventTypes: () => req('GET', '/schedule/event-types'), createEventType: (body) => req('POST', '/schedule/event-types', body), updateEventType: (id, body) => req('PATCH', `/schedule/event-types/${id}`, body), deleteEventType: (id) => req('DELETE', `/schedule/event-types/${id}`), getEvents: (params = {}) => { const qs = new URLSearchParams(params).toString(); return req('GET', `/schedule${qs ? '?' + qs : ''}`); }, getEvent: (id) => req('GET', `/schedule/${id}`), createEvent: (body) => req('POST', '/schedule', body), // body may include recurrenceRule: {freq,interval,byDay,ends,endDate,endCount} updateEvent: (id, body) => req('PATCH', `/schedule/${id}`, body), deleteEvent: (id, scope = 'this') => req('DELETE', `/schedule/${id}`, { recurringScope: scope }), setAvailability: (id, response, note) => req('PUT', `/schedule/${id}/availability`, { response, note }), setAvailabilityNote: (id, note) => req('PATCH', `/schedule/${id}/availability/note`, { note }), deleteAvailability: (id) => req('DELETE', `/schedule/${id}/availability`), getPendingAvailability: () => req('GET', '/schedule/me/pending'), bulkAvailability: (responses) => req('POST', '/schedule/me/bulk-availability', { responses }), importPreview: (file) => { const fd = new FormData(); fd.append('file', file); return fetch('/api/schedule/import/preview', { method: 'POST', headers: { Authorization: 'Bearer ' + localStorage.getItem('tc_token') }, body: fd }).then(r => r.json()); }, importConfirm: (rows) => req('POST', '/schedule/import/confirm', { rows }), // User groups (Group Manager) getMyUserGroups: () => req('GET', '/usergroups/me'), getUserGroups: () => req('GET', '/usergroups'), getUserGroup: (id) => req('GET', `/usergroups/${id}`), getUserGroupsForUser: (userId) => req('GET', `/usergroups/byuser/${userId}`), createUserGroup: (body) => req('POST', '/usergroups', body), updateUserGroup: (id, body) => req('PATCH', `/usergroups/${id}`, body), deleteUserGroup: (id) => req('DELETE', `/usergroups/${id}`), addUserToGroup: (groupId, userId) => req('POST', `/usergroups/${groupId}/members/${userId}`, {}), removeUserFromGroup: (groupId, userId) => req('DELETE', `/usergroups/${groupId}/members/${userId}`), removeUserGroupMember: (groupId, userId) => req('DELETE', `/usergroups/${groupId}/members/${userId}`), // Multi-group DMs getMultiGroupDms: () => req('GET', '/usergroups/multigroup'), createMultiGroupDm: (body) => req('POST', '/usergroups/multigroup', body), updateMultiGroupDm: (id, body) => req('PATCH', `/usergroups/multigroup/${id}`, body), deleteMultiGroupDm: (id) => req('DELETE', `/usergroups/multigroup/${id}`), // U2U Restrictions getGroupRestrictions: (id) => req('GET', `/usergroups/${id}/restrictions`), setGroupRestrictions: (id, blockedGroupIds) => req('PUT', `/usergroups/${id}/restrictions`, { blockedGroupIds }), uploadLogo: (file) => { const form = new FormData(); form.append('logo', file); return req('POST', '/settings/logo', form); }, uploadIconNewChat: (file) => { const form = new FormData(); form.append('icon', file); return req('POST', '/settings/icon-newchat', form); }, uploadIconGroupInfo: (file) => { const form = new FormData(); form.append('icon', file); return req('POST', '/settings/icon-groupinfo', form); }, resetSettings: () => req('POST', '/settings/reset'), // Push notifications (FCM) getFirebaseConfig: () => req('GET', '/push/firebase-config'), getVapidPublicKey: () => req('GET', '/push/vapid-public-key'), subscribePush: (fcmToken) => req('POST', '/push/subscribe', { fcmToken }), subscribeWebPush: (subscription) => req('POST', '/push/subscribe-webpush', subscription), unsubscribePush: () => req('POST', '/push/unsubscribe'), testPush: (mode = 'notification') => req('POST', `/push/test?mode=${mode}`), pushDebug: () => req('GET', '/push/debug'), // Link preview getLinkPreview: (url) => req('GET', `/link-preview?url=${encodeURIComponent(url)}`), };