diff --git a/fe/quasar.config.ts b/fe/quasar.config.ts
index a59d2da..7a12b60 100644
--- a/fe/quasar.config.ts
+++ b/fe/quasar.config.ts
@@ -162,12 +162,12 @@ export default defineConfig((ctx) => {
 
     // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
     pwa: {
-      workboxMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
-      // swFilename: 'sw.js',
-      // manifestFilename: 'manifest.json',
+      workboxMode: 'InjectManifest', // Changed from 'GenerateSW' to 'InjectManifest'
+      swFilename: 'sw.js',
+      manifestFilename: 'manifest.json',
+      injectPwaMetaTags: true,
       // extendManifestJson (json) {},
       // useCredentialsForManifestTag: true,
-      // injectPwaMetaTags: false,
       // extendPWACustomSWConf (esbuildConf) {},
       // extendGenerateSWOptions (cfg) {},
       // extendInjectManifestOptions (cfg) {}
diff --git a/fe/src-pwa/custom-service-worker.ts b/fe/src-pwa/custom-service-worker.ts
index 3201f3a..883c1cd 100644
--- a/fe/src-pwa/custom-service-worker.ts
+++ b/fe/src-pwa/custom-service-worker.ts
@@ -14,6 +14,10 @@ import {
   createHandlerBoundToURL,
 } from 'workbox-precaching';
 import { registerRoute, NavigationRoute } from 'workbox-routing';
+import { CacheFirst, NetworkFirst } from 'workbox-strategies';
+import { ExpirationPlugin } from 'workbox-expiration';
+import { CacheableResponsePlugin } from 'workbox-cacheable-response';
+import type { WorkboxPlugin } from 'workbox-core/types';
 
 self.skipWaiting().catch((error) => {
   console.error('Error during service worker activation:', error);
@@ -25,6 +29,46 @@ precacheAndRoute(self.__WB_MANIFEST);
 
 cleanupOutdatedCaches();
 
+// Cache app shell and static assets with Cache First strategy
+registerRoute(
+  // Match static assets
+  ({ request }) =>
+    request.destination === 'style' ||
+    request.destination === 'script' ||
+    request.destination === 'image' ||
+    request.destination === 'font',
+  new CacheFirst({
+    cacheName: 'static-assets',
+    plugins: [
+      new CacheableResponsePlugin({
+        statuses: [0, 200],
+      }) as WorkboxPlugin,
+      new ExpirationPlugin({
+        maxEntries: 60,
+        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
+      }) as WorkboxPlugin,
+    ],
+  })
+);
+
+// Cache API calls with Network First strategy
+registerRoute(
+  // Match API calls
+  ({ url }) => url.pathname.startsWith('/api/'),
+  new NetworkFirst({
+    cacheName: 'api-cache',
+    plugins: [
+      new CacheableResponsePlugin({
+        statuses: [0, 200],
+      }) as WorkboxPlugin,
+      new ExpirationPlugin({
+        maxEntries: 50,
+        maxAgeSeconds: 24 * 60 * 60, // 24 hours
+      }) as WorkboxPlugin,
+    ],
+  })
+);
+
 // Non-SSR fallbacks to index.html
 // Production SSR fallbacks to offline.html (except for dev)
 if (process.env.MODE !== 'ssr' || process.env.PROD) {
diff --git a/fe/src/components/ConflictResolutionDialog.vue b/fe/src/components/ConflictResolutionDialog.vue
new file mode 100644
index 0000000..7733cfa
--- /dev/null
+++ b/fe/src/components/ConflictResolutionDialog.vue
@@ -0,0 +1,288 @@
+<template>
+  <q-dialog v-model="show" persistent>
+    <q-card style="min-width: 600px">
+      <q-card-section>
+        <div class="text-h6">Conflict Resolution</div>
+        <div class="text-subtitle2 q-mt-sm">
+          This item was modified while you were offline. Please review the changes and choose how to resolve the conflict.
+        </div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none">
+        <q-tabs
+          v-model="activeTab"
+          class="text-primary"
+          active-color="primary"
+          indicator-color="primary"
+          align="justify"
+          narrow-indicator
+        >
+          <q-tab name="compare" label="Compare Versions" />
+          <q-tab name="merge" label="Merge Changes" />
+        </q-tabs>
+
+        <q-tab-panels v-model="activeTab" animated>
+          <!-- Compare Versions Tab -->
+          <q-tab-panel name="compare">
+            <div class="row q-col-gutter-md">
+              <!-- Local Version -->
+              <div class="col-6">
+                <q-card flat bordered>
+                  <q-card-section>
+                    <div class="text-subtitle1">Your Version</div>
+                    <div class="text-caption">
+                      Last modified: {{ formatDate(conflictData?.localVersion.timestamp ?? 0) }}
+                    </div>
+                  </q-card-section>
+                  <q-card-section class="q-pt-none">
+                    <q-list>
+                      <q-item v-for="(value, key) in conflictData?.localVersion.data" :key="key">
+                        <q-item-section>
+                          <q-item-label class="text-caption text-grey">
+                            {{ formatKey(key) }}
+                          </q-item-label>
+                          <q-item-label :class="{ 'text-positive': isDifferent(key) }">
+                            {{ formatValue(value) }}
+                          </q-item-label>
+                        </q-item-section>
+                      </q-item>
+                    </q-list>
+                  </q-card-section>
+                </q-card>
+              </div>
+
+              <!-- Server Version -->
+              <div class="col-6">
+                <q-card flat bordered>
+                  <q-card-section>
+                    <div class="text-subtitle1">Server Version</div>
+                    <div class="text-caption">
+                      Last modified: {{ formatDate(conflictData?.serverVersion.timestamp ?? 0) }}
+                    </div>
+                  </q-card-section>
+                  <q-card-section class="q-pt-none">
+                    <q-list>
+                      <q-item v-for="(value, key) in conflictData?.serverVersion.data" :key="key">
+                        <q-item-section>
+                          <q-item-label class="text-caption text-grey">
+                            {{ formatKey(key) }}
+                          </q-item-label>
+                          <q-item-label :class="{ 'text-positive': isDifferent(key) }">
+                            {{ formatValue(value) }}
+                          </q-item-label>
+                        </q-item-section>
+                      </q-item>
+                    </q-list>
+                  </q-card-section>
+                </q-card>
+              </div>
+            </div>
+          </q-tab-panel>
+
+          <!-- Merge Changes Tab -->
+          <q-tab-panel name="merge">
+            <q-card flat bordered>
+              <q-card-section>
+                <div class="text-subtitle1">Merge Changes</div>
+                <div class="text-caption">
+                  Select which version to keep for each field
+                </div>
+              </q-card-section>
+              <q-card-section class="q-pt-none">
+                <q-list>
+                  <q-item v-for="(value, key) in conflictData?.localVersion.data" :key="key">
+                    <q-item-section>
+                      <q-item-label class="text-caption text-grey">
+                        {{ formatKey(key) }}
+                      </q-item-label>
+                      <div class="row q-col-gutter-sm q-mt-xs">
+                        <div class="col">
+                          <q-radio
+                            v-model="mergeChoices[key]"
+                            val="local"
+                            label="Your Version"
+                          />
+                          <div class="text-caption">
+                            {{ formatValue(value) }}
+                          </div>
+                        </div>
+                        <div class="col">
+                          <q-radio
+                            v-model="mergeChoices[key]"
+                            val="server"
+                            label="Server Version"
+                          />
+                          <div class="text-caption">
+                            {{ formatValue(conflictData?.serverVersion.data[key]) }}
+                          </div>
+                        </div>
+                      </div>
+                    </q-item-section>
+                  </q-item>
+                </q-list>
+              </q-card-section>
+            </q-card>
+          </q-tab-panel>
+        </q-tab-panels>
+      </q-card-section>
+
+      <q-card-actions align="right">
+        <q-btn
+          v-if="activeTab === 'compare'"
+          flat
+          label="Keep Local Version"
+          color="primary"
+          @click="resolveConflict('local')"
+        />
+        <q-btn
+          v-if="activeTab === 'compare'"
+          flat
+          label="Keep Server Version"
+          color="primary"
+          @click="resolveConflict('server')"
+        />
+        <q-btn
+          v-if="activeTab === 'compare'"
+          flat
+          label="Merge Changes"
+          color="primary"
+          @click="activeTab = 'merge'"
+        />
+        <q-btn
+          v-if="activeTab === 'merge'"
+          flat
+          label="Apply Merged Changes"
+          color="primary"
+          @click="applyMergedChanges"
+        />
+        <q-btn
+          flat
+          label="Cancel"
+          color="negative"
+          @click="show = false"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import type { OfflineAction } from 'src/stores/offline';
+
+interface ConflictData {
+  localVersion: {
+    data: Record<string, any>;
+    timestamp: number;
+  };
+  serverVersion: {
+    data: Record<string, any>;
+    timestamp: number;
+  };
+  action: OfflineAction;
+}
+
+const props = defineProps<{
+  modelValue: boolean;
+  conflictData: ConflictData | null;
+}>();
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void;
+  (e: 'resolve', resolution: { version: 'local' | 'server' | 'merge'; action: OfflineAction; mergedData?: Record<string, any> }): void;
+}>();
+
+const show = ref(props.modelValue);
+const activeTab = ref('compare');
+const mergeChoices = ref<Record<string, 'local' | 'server'>>({});
+
+// Watch for changes in modelValue
+watch(() => props.modelValue, (newValue: boolean) => {
+  show.value = newValue;
+});
+
+// Watch for changes in show
+watch(show, (newValue: boolean) => {
+  emit('update:modelValue', newValue);
+});
+
+// Initialize merge choices when conflict data changes
+watch(() => props.conflictData, (newData) => {
+  if (newData) {
+    const choices: Record<string, 'local' | 'server'> = {};
+    Object.keys(newData.localVersion.data).forEach(key => {
+      choices[key] = isDifferent(key) ? 'local' : 'local';
+    });
+    mergeChoices.value = choices;
+  }
+}, { immediate: true });
+
+const formatDate = (timestamp: number): string => {
+  return new Date(timestamp).toLocaleString();
+};
+
+const formatKey = (key: string): string => {
+  return key
+    .split(/(?=[A-Z])/)
+    .join(' ')
+    .toLowerCase()
+    .replace(/^\w/, (c) => c.toUpperCase());
+};
+
+const formatValue = (value: any): string => {
+  if (value === null || value === undefined) return '-';
+  if (typeof value === 'boolean') return value ? 'Yes' : 'No';
+  if (typeof value === 'object') return JSON.stringify(value);
+  return String(value);
+};
+
+const isDifferent = (key: string): boolean => {
+  if (!props.conflictData) return false;
+  const localValue = props.conflictData.localVersion.data[key];
+  const serverValue = props.conflictData.serverVersion.data[key];
+  return JSON.stringify(localValue) !== JSON.stringify(serverValue);
+};
+
+const resolveConflict = (version: 'local' | 'server' | 'merge'): void => {
+  if (!props.conflictData) return;
+  
+  emit('resolve', {
+    version,
+    action: props.conflictData.action
+  });
+  
+  show.value = false;
+};
+
+const applyMergedChanges = (): void => {
+  if (!props.conflictData) return;
+
+  const mergedData: Record<string, any> = {};
+  Object.entries(mergeChoices.value).forEach(([key, choice]) => {
+    const localValue = props.conflictData?.localVersion.data[key];
+    const serverValue = props.conflictData?.serverVersion.data[key];
+    mergedData[key] = choice === 'local' ? localValue : serverValue;
+  });
+
+  emit('resolve', {
+    version: 'merge',
+    action: props.conflictData.action,
+    mergedData
+  });
+
+  show.value = false;
+};
+</script>
+
+<style lang="scss" scoped>
+.q-card {
+  .text-caption {
+    font-size: 0.8rem;
+  }
+}
+
+.text-positive {
+  color: $positive;
+  font-weight: 500;
+}
+</style> 
\ No newline at end of file
diff --git a/fe/src/components/OfflineIndicator.vue b/fe/src/components/OfflineIndicator.vue
new file mode 100644
index 0000000..43ad775
--- /dev/null
+++ b/fe/src/components/OfflineIndicator.vue
@@ -0,0 +1,123 @@
+<template>
+  <q-banner
+    v-if="!isOnline || hasPendingActions"
+    :class="[
+      'offline-indicator',
+      { 'offline': !isOnline },
+      { 'pending': hasPendingActions }
+    ]"
+    rounded
+  >
+    <template v-slot:avatar>
+      <q-icon
+        :name="!isOnline ? 'wifi_off' : 'sync'"
+        :color="!isOnline ? 'negative' : 'warning'"
+      />
+    </template>
+
+    <template v-if="!isOnline">
+      You are currently offline. Changes will be saved locally.
+    </template>
+    <template v-else>
+      Syncing {{ pendingActionCount }} pending {{ pendingActionCount === 1 ? 'change' : 'changes' }}...
+    </template>
+
+    <template v-slot:action>
+      <q-btn
+        v-if="hasPendingActions"
+        flat
+        color="primary"
+        label="View Changes"
+        @click="showPendingActions = true"
+      />
+    </template>
+  </q-banner>
+
+  <q-dialog v-model="showPendingActions">
+    <q-card style="min-width: 350px">
+      <q-card-section>
+        <div class="text-h6">Pending Changes</div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none">
+        <q-list>
+          <q-item v-for="action in pendingActions" :key="action.id">
+            <q-item-section>
+              <q-item-label>
+                {{ getActionLabel(action) }}
+              </q-item-label>
+              <q-item-label caption>
+                {{ new Date(action.timestamp).toLocaleString() }}
+              </q-item-label>
+            </q-item-section>
+          </q-item>
+        </q-list>
+      </q-card-section>
+
+      <q-card-actions align="right">
+        <q-btn flat label="Close" color="primary" v-close-popup />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+
+  <!-- Conflict Resolution Dialog -->
+  <ConflictResolutionDialog
+    v-model="showConflictDialog"
+    :conflict-data="currentConflict"
+    @resolve="handleConflictResolution"
+  />
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useOfflineStore } from 'src/stores/offline';
+import type { OfflineAction } from 'src/stores/offline';
+import ConflictResolutionDialog from './ConflictResolutionDialog.vue';
+
+const offlineStore = useOfflineStore();
+const showPendingActions = ref(false);
+
+const {
+  isOnline,
+  pendingActions,
+  hasPendingActions,
+  pendingActionCount,
+  showConflictDialog,
+  currentConflict,
+  handleConflictResolution,
+} = offlineStore;
+
+const getActionLabel = (action: OfflineAction) => {
+  switch (action.type) {
+    case 'add':
+      return `Add new item: ${action.data.title || 'Untitled'}`;
+    case 'complete':
+      return `Complete item: ${action.data.title || 'Untitled'}`;
+    case 'update':
+      return `Update item: ${action.data.title || 'Untitled'}`;
+    case 'delete':
+      return `Delete item: ${action.data.title || 'Untitled'}`;
+    default:
+      return 'Unknown action';
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.offline-indicator {
+  position: fixed;
+  bottom: 16px;
+  right: 16px;
+  z-index: 1000;
+  max-width: 400px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+
+  &.offline {
+    background-color: #ffebee;
+  }
+
+  &.pending {
+    background-color: #fff3e0;
+  }
+}
+</style> 
\ No newline at end of file
diff --git a/fe/src/layouts/MainLayout.vue b/fe/src/layouts/MainLayout.vue
index 29c5e76..8bbbbee 100644
--- a/fe/src/layouts/MainLayout.vue
+++ b/fe/src/layouts/MainLayout.vue
@@ -32,6 +32,9 @@
       <router-view />
     </q-page-container>
 
+    <!-- Offline Indicator -->
+    <OfflineIndicator />
+
     <!-- Bottom Navigation -->
     <q-footer elevated class="bg-white text-primary">
       <q-tabs
@@ -55,6 +58,7 @@ import { ref } from 'vue';
 import { useRouter } from 'vue-router';
 import { useQuasar } from 'quasar';
 import { useAuthStore } from 'stores/auth';
+import OfflineIndicator from 'components/OfflineIndicator.vue';
 
 const router = useRouter();
 const $q = useQuasar();
diff --git a/fe/src/stores/offline.ts b/fe/src/stores/offline.ts
new file mode 100644
index 0000000..84533f5
--- /dev/null
+++ b/fe/src/stores/offline.ts
@@ -0,0 +1,157 @@
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+import { useQuasar } from 'quasar';
+import { LocalStorage } from 'quasar';
+
+export interface OfflineAction {
+    id: string;
+    type: 'add' | 'complete' | 'update' | 'delete';
+    itemId?: string;
+    data: any;
+    timestamp: number;
+    version?: number;
+}
+
+export interface ConflictResolution {
+    version: 'local' | 'server' | 'merge';
+    action: OfflineAction;
+}
+
+export const useOfflineStore = defineStore('offline', () => {
+    const $q = useQuasar();
+    const isOnline = ref(navigator.onLine);
+    const pendingActions = ref<OfflineAction[]>([]);
+    const isProcessingQueue = ref(false);
+    const showConflictDialog = ref(false);
+    const currentConflict = ref<ConflictResolution | null>(null);
+
+    // Initialize from IndexedDB
+    const init = async () => {
+        try {
+            const stored = LocalStorage.getItem('offline-actions');
+            if (stored) {
+                pendingActions.value = JSON.parse(stored as string);
+            }
+        } catch (error) {
+            console.error('Failed to load offline actions:', error);
+        }
+    };
+
+    // Save to IndexedDB
+    const saveToStorage = () => {
+        try {
+            LocalStorage.set('offline-actions', JSON.stringify(pendingActions.value));
+        } catch (error) {
+            console.error('Failed to save offline actions:', error);
+        }
+    };
+
+    // Add a new offline action
+    const addAction = (action: Omit<OfflineAction, 'id' | 'timestamp'>) => {
+        const newAction: OfflineAction = {
+            ...action,
+            id: crypto.randomUUID(),
+            timestamp: Date.now(),
+        };
+        pendingActions.value.push(newAction);
+        saveToStorage();
+    };
+
+    // Process the queue when online
+    const processQueue = async () => {
+        if (isProcessingQueue.value || !isOnline.value) return;
+
+        isProcessingQueue.value = true;
+        const actions = [...pendingActions.value];
+
+        for (const action of actions) {
+            try {
+                // TODO: Implement actual API calls based on action type
+                // This will be implemented when we have the API endpoints
+                await processAction(action);
+
+                // Remove successful action
+                pendingActions.value = pendingActions.value.filter(a => a.id !== action.id);
+                saveToStorage();
+            } catch (error: any) {
+                if (error.status === 409) {
+                    // Handle version conflict
+                    $q.notify({
+                        type: 'warning',
+                        message: 'Item was modified by someone else while you were offline. Please review.',
+                        actions: [
+                            {
+                                label: 'Review',
+                                color: 'white',
+                                handler: () => {
+                                    // TODO: Implement conflict resolution UI
+                                }
+                            }
+                        ]
+                    });
+                } else {
+                    console.error('Failed to process offline action:', error);
+                }
+            }
+        }
+
+        isProcessingQueue.value = false;
+    };
+
+    // Process a single action
+    const processAction = async (action: OfflineAction) => {
+        // TODO: Implement actual API calls
+        // This is a placeholder that will be replaced with actual API calls
+        switch (action.type) {
+            case 'add':
+                // await api.addItem(action.data);
+                break;
+            case 'complete':
+                // await api.completeItem(action.itemId, action.data);
+                break;
+            case 'update':
+                // await api.updateItem(action.itemId, action.data);
+                break;
+            case 'delete':
+                // await api.deleteItem(action.itemId);
+                break;
+        }
+    };
+
+    // Listen for online/offline status changes
+    const setupNetworkListeners = () => {
+        window.addEventListener('online', () => {
+            isOnline.value = true;
+            processQueue();
+        });
+
+        window.addEventListener('offline', () => {
+            isOnline.value = false;
+        });
+    };
+
+    // Computed properties
+    const hasPendingActions = computed(() => pendingActions.value.length > 0);
+    const pendingActionCount = computed(() => pendingActions.value.length);
+
+    // Initialize
+    init();
+    setupNetworkListeners();
+
+    const handleConflictResolution = (resolution: ConflictResolution) => {
+        // Implement the logic to handle the conflict resolution
+        console.log('Conflict resolution:', resolution);
+    };
+
+    return {
+        isOnline,
+        pendingActions,
+        hasPendingActions,
+        pendingActionCount,
+        showConflictDialog,
+        currentConflict,
+        addAction,
+        processQueue,
+        handleConflictResolution,
+    };
+}); 
\ No newline at end of file