Merge branch 'main' of github.com:rafaelfoster/oikos

This commit is contained in:
Rafael Foster
2026-04-29 07:21:18 -03:00
28 changed files with 2117 additions and 38 deletions
+18 -2
View File
@@ -105,7 +105,7 @@ function trapFocus(container) {
// --------------------------------------------------------
function serializeForm(container) {
const inputs = container.querySelectorAll('input, select, textarea');
const inputs = container.querySelectorAll('input:not([type="file"]), select, textarea');
return Array.from(inputs).map((el) => `${el.name || el.id}=${el.value}`).join('&');
}
@@ -327,17 +327,33 @@ export async function closeModal({ force = false } = {}) {
if (!force) {
const panel = activeOverlay.querySelector('.modal-panel');
if (panel && isFormDirty(panel)) {
const dirtyOverlay = activeOverlay;
const dirtySnapshot = _initialFormSnapshot;
let confirmed;
try {
activeOverlay = null;
_isClosing = false;
confirmed = await confirmModal(t('modal.unsavedChanges'), {
danger: false,
confirmLabel: t('modal.discardChanges'),
});
} catch (err) {
activeOverlay = dirtyOverlay;
_initialFormSnapshot = dirtySnapshot;
_isClosing = false;
throw err;
}
if (!confirmed) { _isClosing = false; return; }
activeOverlay = dirtyOverlay;
_initialFormSnapshot = dirtySnapshot;
if (!confirmed) {
document.body.style.overflow = 'hidden';
if (window.oikos?.setThemeColor) {
window.oikos.setThemeColor(OVERLAY_THEME_COLOR, OVERLAY_THEME_COLOR);
}
_isClosing = false;
return;
}
_isClosing = true;
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "التنقل",
"quickActions": "الإجراءات السريعة",
"recipes": "الوصفات",
"more": "المزيد"
"more": "المزيد",
"documents": "المستندات"
},
"dashboard": {
"title": "لوحة التحكم",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "أنشئ وصفات واربطها بمخطط الوجبات."
},
"documents": {
"title": "المستندات",
"addButton": "إضافة مستند",
"searchPlaceholder": "البحث في المستندات...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "كل الفئات",
"emptyTitle": "لا توجد مستندات بعد",
"emptyDescription": "ارفع مستندات العائلة وتحكم في من يمكنه رؤية كل ملف.",
"newTitle": "مستند جديد",
"editTitle": "إعدادات المستند",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "كل العائلة",
"restricted": "أعضاء محددون",
"private": "أنا فقط"
},
"category": {
"medical": "طبي",
"school": "مدرسة",
"identity": "هوية",
"insurance": "تأمين",
"finance": "مالية",
"home": "منزل",
"vehicle": "مركبة",
"legal": "قانوني",
"travel": "سفر",
"pets": "حيوانات أليفة",
"warranty": "ضمان",
"taxes": "ضرائب",
"work": "عمل",
"other": "أخرى"
},
"dropzoneTitle": "أفلت الملف هنا أو انقر للاختيار",
"dropzoneHint": "اسحب ملفًا إلى هذه المنطقة أو استخدم محدد الملفات.",
"selectedFileLabel": "المحدد: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Navigation",
"quickActions": "Schnellaktionen",
"more": "Mehr",
"recipes": "Rezepte"
"recipes": "Rezepte",
"documents": "Dokumente"
},
"search": {
"title": "Suche",
@@ -935,5 +936,66 @@
"goCal": "Kalender",
"goShop": "Einkaufsliste",
"goNotes": "Notizen"
},
"documents": {
"title": "Dokumente",
"addButton": "Dokument hinzufügen",
"searchPlaceholder": "Dokumente suchen...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Alle Kategorien",
"emptyTitle": "Noch keine Dokumente",
"emptyDescription": "Lade Familiendokumente hoch und steuere, wer jede Datei sehen darf.",
"newTitle": "Neues Dokument",
"editTitle": "Dokumenteinstellungen",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Ganze Familie",
"restricted": "Ausgewählte Mitglieder",
"private": "Nur ich"
},
"category": {
"medical": "Medizin",
"school": "Schule",
"identity": "Identität",
"insurance": "Versicherung",
"finance": "Finanzen",
"home": "Zuhause",
"vehicle": "Fahrzeug",
"legal": "Rechtliches",
"travel": "Reisen",
"pets": "Haustiere",
"warranty": "Garantie",
"taxes": "Steuern",
"work": "Arbeit",
"other": "Sonstiges"
},
"dropzoneTitle": "Datei hier ablegen oder klicken",
"dropzoneHint": "Ziehe eine Datei in diesen Bereich oder nutze die Dateiauswahl.",
"selectedFileLabel": "Ausgewählt: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Πλοήγηση",
"quickActions": "Γρήγορες ενέργειες",
"recipes": "Συνταγές",
"more": "Περισσότερα"
"more": "Περισσότερα",
"documents": "Έγγραφα"
},
"dashboard": {
"title": "Επισκόπηση",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "Δημιουργήστε συνταγές και συνδέστε τις με τον προγραμματισμό γευμάτων."
},
"documents": {
"title": "Έγγραφα",
"addButton": "Προσθήκη εγγράφου",
"searchPlaceholder": "Αναζήτηση εγγράφων...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Όλες οι κατηγορίες",
"emptyTitle": "Δεν υπάρχουν έγγραφα ακόμα",
"emptyDescription": "Ανεβάστε οικογενειακά έγγραφα και ελέγξτε ποιος μπορεί να βλέπει κάθε αρχείο.",
"newTitle": "Νέο έγγραφο",
"editTitle": "Ρυθμίσεις εγγράφου",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Όλη η οικογένεια",
"restricted": "Επιλεγμένα μέλη",
"private": "Μόνο εγώ"
},
"category": {
"medical": "Ιατρικά",
"school": "Σχολείο",
"identity": "Ταυτότητα",
"insurance": "Ασφάλιση",
"finance": "Οικονομικά",
"home": "Σπίτι",
"vehicle": "Όχημα",
"legal": "Νομικά",
"travel": "Ταξίδια",
"pets": "Κατοικίδια",
"warranty": "Εγγύηση",
"taxes": "Φόροι",
"work": "Εργασία",
"other": "Άλλο"
},
"dropzoneTitle": "Αφήστε το αρχείο εδώ ή κάντε κλικ για επιλογή",
"dropzoneHint": "Σύρετε ένα αρχείο σε αυτήν την περιοχή ή χρησιμοποιήστε τον επιλογέα αρχείων.",
"selectedFileLabel": "Επιλέχθηκε: {{name}}"
}
}
}
+63 -1
View File
@@ -46,7 +46,8 @@
"navigation": "Navigation",
"quickActions": "Quick actions",
"recipes": "Recipes",
"more": "More"
"more": "More",
"documents": "Documents"
},
"dashboard": {
"title": "Overview",
@@ -921,5 +922,66 @@
"meals": "Plan meals for the week and link recipes.",
"birthdays": "Add birthdays — you will receive a reminder in time.",
"recipes": "Create recipes and link them to your meal planner."
},
"documents": {
"title": "Documents",
"addButton": "Add document",
"searchPlaceholder": "Search documents...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "All categories",
"emptyTitle": "No documents yet",
"emptyDescription": "Upload family documents and control who can see each file.",
"newTitle": "New document",
"editTitle": "Document settings",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Entire family",
"restricted": "Selected members",
"private": "Only me"
},
"category": {
"medical": "Medical",
"school": "School",
"identity": "Identity",
"insurance": "Insurance",
"finance": "Finance",
"home": "Home",
"vehicle": "Vehicle",
"legal": "Legal",
"travel": "Travel",
"pets": "Pets",
"warranty": "Warranty",
"taxes": "Taxes",
"work": "Work",
"other": "Other"
},
"dropzoneTitle": "Drop file here or click to choose",
"dropzoneHint": "Drag a file into this area, or use the file picker.",
"selectedFileLabel": "Selected: {{name}}"
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Navegación",
"quickActions": "Acciones rápidas",
"recipes": "Recetas",
"more": "Más"
"more": "Más",
"documents": "Documentos"
},
"dashboard": {
"title": "Inicio",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "Crea recetas y vincúlalas con tu planificador de comidas."
},
"documents": {
"title": "Documentos",
"addButton": "Agregar documento",
"searchPlaceholder": "Buscar documentos...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Todas las categorías",
"emptyTitle": "Aún no hay documentos",
"emptyDescription": "Sube documentos familiares y controla quién puede ver cada archivo.",
"newTitle": "Nuevo documento",
"editTitle": "Configuración del documento",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Toda la familia",
"restricted": "Miembros seleccionados",
"private": "Solo yo"
},
"category": {
"medical": "Médico",
"school": "Escuela",
"identity": "Identidad",
"insurance": "Seguro",
"finance": "Finanzas",
"home": "Hogar",
"vehicle": "Vehículo",
"legal": "Legal",
"travel": "Viajes",
"pets": "Mascotas",
"warranty": "Garantía",
"taxes": "Impuestos",
"work": "Trabajo",
"other": "Otros"
},
"dropzoneTitle": "Suelta el archivo aquí o haz clic para elegir",
"dropzoneHint": "Arrastra un archivo a esta área o usa el selector de archivos.",
"selectedFileLabel": "Seleccionado: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Navigation",
"quickActions": "Actions rapides",
"recipes": "Recettes",
"more": "Plus"
"more": "Plus",
"documents": "Documents"
},
"dashboard": {
"title": "Accueil",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "Créez des recettes et associez-les à votre planification des repas."
},
"documents": {
"title": "Documents",
"addButton": "Ajouter un document",
"searchPlaceholder": "Rechercher des documents...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Toutes les catégories",
"emptyTitle": "Aucun document pour le moment",
"emptyDescription": "Ajoutez des documents familiaux et contrôlez qui peut voir chaque fichier.",
"newTitle": "Nouveau document",
"editTitle": "Paramètres du document",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Toute la famille",
"restricted": "Membres sélectionnés",
"private": "Moi uniquement"
},
"category": {
"medical": "Médical",
"school": "École",
"identity": "Identité",
"insurance": "Assurance",
"finance": "Finances",
"home": "Maison",
"vehicle": "Véhicule",
"legal": "Juridique",
"travel": "Voyage",
"pets": "Animaux",
"warranty": "Garantie",
"taxes": "Impôts",
"work": "Travail",
"other": "Autre"
},
"dropzoneTitle": "Déposez le fichier ici ou cliquez pour choisir",
"dropzoneHint": "Glissez un fichier dans cette zone ou utilisez le sélecteur.",
"selectedFileLabel": "Sélectionné : {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "नेविगेशन",
"quickActions": "त्वरित क्रियाएं",
"recipes": "रेसिपी",
"more": "और"
"more": "और",
"documents": "दस्तावेज़"
},
"dashboard": {
"title": "डैशबोर्ड",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "रेसिपी बनाएं और उन्हें अपने भोजन योजनाकार से जोड़ें।"
},
"documents": {
"title": "दस्तावेज़",
"addButton": "दस्तावेज़ जोड़ें",
"searchPlaceholder": "दस्तावेज़ खोजें...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "सभी श्रेणियाँ",
"emptyTitle": "अभी कोई दस्तावेज़ नहीं",
"emptyDescription": "परिवार के दस्तावेज़ अपलोड करें और तय करें कि हर फ़ाइल कौन देख सकता है।",
"newTitle": "नया दस्तावेज़",
"editTitle": "दस्तावेज़ सेटिंग्स",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "पूरा परिवार",
"restricted": "चुने हुए सदस्य",
"private": "केवल मैं"
},
"category": {
"medical": "चिकित्सा",
"school": "स्कूल",
"identity": "पहचान",
"insurance": "बीमा",
"finance": "वित्त",
"home": "घर",
"vehicle": "वाहन",
"legal": "कानूनी",
"travel": "यात्रा",
"pets": "पालतू",
"warranty": "वारंटी",
"taxes": "कर",
"work": "काम",
"other": "अन्य"
},
"dropzoneTitle": "फ़ाइल यहाँ छोड़ें या चुनने के लिए क्लिक करें",
"dropzoneHint": "फ़ाइल को इस क्षेत्र में खींचें या फ़ाइल पिकर का उपयोग करें।",
"selectedFileLabel": "चयनित: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Navigazione",
"quickActions": "Azioni rapide",
"recipes": "Ricette",
"more": "Altro"
"more": "Altro",
"documents": "Documenti"
},
"dashboard": {
"title": "Panoramica",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "Crea ricette e collegale al tuo piano pasti."
},
"documents": {
"title": "Documenti",
"addButton": "Aggiungi documento",
"searchPlaceholder": "Cerca documenti...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Tutte le categorie",
"emptyTitle": "Nessun documento",
"emptyDescription": "Carica documenti di famiglia e controlla chi può vedere ogni file.",
"newTitle": "Nuovo documento",
"editTitle": "Impostazioni documento",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Tutta la famiglia",
"restricted": "Membri selezionati",
"private": "Solo io"
},
"category": {
"medical": "Medico",
"school": "Scuola",
"identity": "Identità",
"insurance": "Assicurazione",
"finance": "Finanze",
"home": "Casa",
"vehicle": "Veicolo",
"legal": "Legale",
"travel": "Viaggi",
"pets": "Animali",
"warranty": "Garanzia",
"taxes": "Tasse",
"work": "Lavoro",
"other": "Altro"
},
"dropzoneTitle": "Rilascia il file qui o fai clic per scegliere",
"dropzoneHint": "Trascina un file in questarea oppure usa il selettore.",
"selectedFileLabel": "Selezionato: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "ナビゲーション",
"quickActions": "クイックアクション",
"recipes": "レシピ",
"more": "もっと見る"
"more": "もっと見る",
"documents": "書類"
},
"dashboard": {
"title": "ダッシュボード",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "レシピを作成して、食事プランに関連付けましょう。"
},
"documents": {
"title": "書類",
"addButton": "書類を追加",
"searchPlaceholder": "書類を検索...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "すべてのカテゴリ",
"emptyTitle": "書類はまだありません",
"emptyDescription": "家族の書類をアップロードし、各ファイルを見られるメンバーを管理できます。",
"newTitle": "新しい書類",
"editTitle": "書類設定",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "家族全員",
"restricted": "選択したメンバー",
"private": "自分のみ"
},
"category": {
"medical": "医療",
"school": "学校",
"identity": "本人確認",
"insurance": "保険",
"finance": "金融",
"home": "家",
"vehicle": "車両",
"legal": "法務",
"travel": "旅行",
"pets": "ペット",
"warranty": "保証",
"taxes": "税金",
"work": "仕事",
"other": "その他"
},
"dropzoneTitle": "ここにファイルをドロップ、またはクリックして選択",
"dropzoneHint": "この領域にファイルをドラッグするか、ファイル選択を使用します。",
"selectedFileLabel": "選択済み: {{name}}"
}
}
}
+63 -1
View File
@@ -46,7 +46,8 @@
"navigation": "Navegação",
"quickActions": "Ações rápidas",
"recipes": "Receitas",
"more": "Mais"
"more": "Mais",
"documents": "Documentos"
},
"dashboard": {
"title": "Painel",
@@ -903,5 +904,66 @@
},
"emptyHint": {
"recipes": "Crie receitas e vincule-as ao seu planejador de refeições."
},
"documents": {
"title": "Documentos",
"addButton": "Adicionar documento",
"searchPlaceholder": "Buscar documentos...",
"gridView": "Visualizacao em grade",
"listView": "Visualizacao em lista",
"viewToggle": "Visualizacao de documentos",
"allCategories": "Todas as categorias",
"emptyTitle": "Nenhum documento ainda",
"emptyDescription": "Envie documentos da familia e controle quem pode ver cada arquivo.",
"newTitle": "Novo documento",
"editTitle": "Configuracoes do documento",
"nameLabel": "Nome",
"descriptionLabel": "Descricao",
"categoryLabel": "Categoria",
"fileLabel": "Arquivo",
"fileHint": "PDF, imagens, texto e arquivos Office ate 5 MB.",
"visibilityLabel": "Visibilidade",
"statusLabel": "Status",
"allowedMembersLabel": "Membros permitidos",
"uploadAction": "Enviar",
"downloadAction": "Baixar",
"editAction": "Configuracoes",
"archiveAction": "Arquivar",
"restoreAction": "Restaurar",
"savedToast": "Documento salvo.",
"uploadedToast": "Documento enviado.",
"archivedToast": "Documento arquivado.",
"restoredToast": "Documento restaurado.",
"deletedToast": "Documento excluido.",
"deleteConfirm": "Excluir documento \"{{name}}\"?",
"fileRequired": "Selecione um arquivo para enviar.",
"fileTooLarge": "O arquivo pode ter no maximo 5 MB.",
"fileReadError": "Nao foi possivel ler o arquivo.",
"statusActive": "Ativo",
"statusArchived": "Arquivado",
"visibility": {
"family": "Familia inteira",
"restricted": "Membros selecionados",
"private": "Somente eu"
},
"category": {
"medical": "Medico",
"school": "Escola",
"identity": "Identidade",
"insurance": "Seguro",
"finance": "Financeiro",
"home": "Casa",
"vehicle": "Veiculo",
"legal": "Juridico",
"travel": "Viagem",
"pets": "Pets",
"warranty": "Garantia",
"taxes": "Impostos",
"work": "Trabalho",
"other": "Outros"
},
"dropzoneTitle": "Solte o arquivo aqui ou clique para escolher",
"dropzoneHint": "Arraste um arquivo para esta area, ou use o seletor de arquivos.",
"selectedFileLabel": "Selecionado: {{name}}"
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Навигация",
"quickActions": "Быстрые действия",
"recipes": "Рецепты",
"more": "Ещё"
"more": "Ещё",
"documents": "Документы"
},
"dashboard": {
"title": "Обзор",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "Создавайте рецепты и связывайте их с вашим планом питания."
},
"documents": {
"title": "Документы",
"addButton": "Добавить документ",
"searchPlaceholder": "Поиск документов...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Все категории",
"emptyTitle": "Документов пока нет",
"emptyDescription": "Загружайте семейные документы и управляйте доступом к каждому файлу.",
"newTitle": "Новый документ",
"editTitle": "Настройки документа",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Вся семья",
"restricted": "Выбранные участники",
"private": "Только я"
},
"category": {
"medical": "Медицина",
"school": "Школа",
"identity": "Удостоверения",
"insurance": "Страхование",
"finance": "Финансы",
"home": "Дом",
"vehicle": "Автомобиль",
"legal": "Юридическое",
"travel": "Путешествия",
"pets": "Питомцы",
"warranty": "Гарантия",
"taxes": "Налоги",
"work": "Работа",
"other": "Другое"
},
"dropzoneTitle": "Перетащите файл сюда или нажмите для выбора",
"dropzoneHint": "Перетащите файл в эту область или используйте выбор файла.",
"selectedFileLabel": "Выбрано: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Navigering",
"quickActions": "Snabba åtgärder",
"recipes": "Recept",
"more": "Mer"
"more": "Mer",
"documents": "Dokument"
},
"dashboard": {
"title": "Översikt",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "Skapa recept och koppla dem till din måltidsplanering."
},
"documents": {
"title": "Dokument",
"addButton": "Lägg till dokument",
"searchPlaceholder": "Sök dokument...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Alla kategorier",
"emptyTitle": "Inga dokument ännu",
"emptyDescription": "Ladda upp familjedokument och styr vem som kan se varje fil.",
"newTitle": "Nytt dokument",
"editTitle": "Dokumentinställningar",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Hela familjen",
"restricted": "Valda medlemmar",
"private": "Endast jag"
},
"category": {
"medical": "Medicinskt",
"school": "Skola",
"identity": "Identitet",
"insurance": "Försäkring",
"finance": "Ekonomi",
"home": "Hem",
"vehicle": "Fordon",
"legal": "Juridiskt",
"travel": "Resor",
"pets": "Husdjur",
"warranty": "Garanti",
"taxes": "Skatter",
"work": "Arbete",
"other": "Övrigt"
},
"dropzoneTitle": "Släpp filen här eller klicka för att välja",
"dropzoneHint": "Dra en fil till området eller använd filväljaren.",
"selectedFileLabel": "Vald: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Gezinme",
"quickActions": "Hızlı işlemler",
"recipes": "Tarifler",
"more": "Daha Fazla"
"more": "Daha Fazla",
"documents": "Belgeler"
},
"dashboard": {
"title": "Genel Bakış",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "Tarifler oluşturun ve yemek planlayıcınıza bağlayın."
},
"documents": {
"title": "Belgeler",
"addButton": "Belge ekle",
"searchPlaceholder": "Belgelerde ara...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Tüm kategoriler",
"emptyTitle": "Henüz belge yok",
"emptyDescription": "Aile belgelerini yükleyin ve her dosyayı kimlerin görebileceğini yönetin.",
"newTitle": "Yeni belge",
"editTitle": "Belge ayarları",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Tüm aile",
"restricted": "Seçili üyeler",
"private": "Sadece ben"
},
"category": {
"medical": "Tıbbi",
"school": "Okul",
"identity": "Kimlik",
"insurance": "Sigorta",
"finance": "Finans",
"home": "Ev",
"vehicle": "Araç",
"legal": "Hukuki",
"travel": "Seyahat",
"pets": "Evcil hayvanlar",
"warranty": "Garanti",
"taxes": "Vergiler",
"work": "İş",
"other": "Diğer"
},
"dropzoneTitle": "Dosyayı buraya bırakın veya seçmek için tıklayın",
"dropzoneHint": "Bir dosyayı bu alana sürükleyin veya dosya seçiciyi kullanın.",
"selectedFileLabel": "Seçildi: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "Навігація",
"quickActions": "Швидкі дії",
"recipes": "Рецепти",
"more": "Більше"
"more": "Більше",
"documents": "Документи"
},
"dashboard": {
"title": "Огляд",
@@ -905,5 +906,66 @@
"meals": "Плануйте харчування на тиждень і пов'язуйте рецепти.",
"birthdays": "Додайте дні народження — ви отримаєте нагадування завчасно.",
"recipes": "Створюйте рецепти та пов'язуйте їх із планувальником харчування."
},
"documents": {
"title": "Документи",
"addButton": "Додати документ",
"searchPlaceholder": "Пошук документів...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "Усі категорії",
"emptyTitle": "Документів ще немає",
"emptyDescription": "Завантажуйте сімейні документи та керуйте доступом до кожного файлу.",
"newTitle": "Новий документ",
"editTitle": "Налаштування документа",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "Уся сім’я",
"restricted": "Вибрані учасники",
"private": "Лише я"
},
"category": {
"medical": "Медицина",
"school": "Школа",
"identity": "Посвідчення",
"insurance": "Страхування",
"finance": "Фінанси",
"home": "Дім",
"vehicle": "Авто",
"legal": "Юридичне",
"travel": "Подорожі",
"pets": "Тварини",
"warranty": "Гарантія",
"taxes": "Податки",
"work": "Робота",
"other": "Інше"
},
"dropzoneTitle": "Перетягніть файл сюди або натисніть для вибору",
"dropzoneHint": "Перетягніть файл у цю область або скористайтеся вибором файлу.",
"selectedFileLabel": "Вибрано: {{name}}"
}
}
}
+64 -2
View File
@@ -46,7 +46,8 @@
"navigation": "导航",
"quickActions": "快捷操作",
"recipes": "食谱",
"more": "更多"
"more": "更多",
"documents": "文档"
},
"dashboard": {
"title": "概览",
@@ -897,5 +898,66 @@
},
"emptyHint": {
"recipes": "创建食谱并将其关联到你的膳食计划。"
},
"documents": {
"title": "文档",
"addButton": "添加文档",
"searchPlaceholder": "搜索文档...",
"gridView": "Grid view",
"listView": "List view",
"viewToggle": "Document view",
"allCategories": "所有类别",
"emptyTitle": "还没有文档",
"emptyDescription": "上传家庭文档并控制每个文件的可见成员。",
"newTitle": "新文档",
"editTitle": "文档设置",
"nameLabel": "Name",
"descriptionLabel": "Description",
"categoryLabel": "Category",
"fileLabel": "File",
"fileHint": "PDF, images, text and Office files up to 5 MB.",
"visibilityLabel": "Visibility",
"statusLabel": "Status",
"allowedMembersLabel": "Allowed members",
"uploadAction": "Upload",
"downloadAction": "Download",
"editAction": "Settings",
"archiveAction": "Archive",
"restoreAction": "Restore",
"savedToast": "Document saved.",
"uploadedToast": "Document uploaded.",
"archivedToast": "Document archived.",
"restoredToast": "Document restored.",
"deletedToast": "Document deleted.",
"deleteConfirm": "Delete document \"{{name}}\"?",
"fileRequired": "Select a file to upload.",
"fileTooLarge": "File may be at most 5 MB.",
"fileReadError": "File could not be read.",
"statusActive": "Active",
"statusArchived": "Archived",
"visibility": {
"family": "整个家庭",
"restricted": "选定成员",
"private": "仅我"
},
"category": {
"medical": "医疗",
"school": "学校",
"identity": "身份",
"insurance": "保险",
"finance": "财务",
"home": "家庭",
"vehicle": "车辆",
"legal": "法律",
"travel": "旅行",
"pets": "宠物",
"warranty": "保修",
"taxes": "税务",
"work": "工作",
"other": "其他"
},
"dropzoneTitle": "将文件拖到此处或点击选择",
"dropzoneHint": "将文件拖入此区域,或使用文件选择器。",
"selectedFileLabel": "已选择:{{name}}"
}
}
}
+421
View File
@@ -0,0 +1,421 @@
/**
* Module: Family Documents
* Purpose: Grid/list document management with local uploads and member visibility.
* Dependencies: /api.js, shared modal, i18n
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
import { t, formatDate } from '/i18n.js';
import { esc } from '/utils/html.js';
import { stagger } from '/utils/ux.js';
const CATEGORIES = ['medical', 'school', 'identity', 'insurance', 'finance', 'home', 'vehicle', 'legal', 'travel', 'pets', 'warranty', 'taxes', 'work', 'other'];
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const CATEGORY_ICONS = {
medical: 'heart-pulse',
school: 'graduation-cap',
identity: 'badge-check',
insurance: 'shield-check',
finance: 'landmark',
home: 'home',
vehicle: 'car',
legal: 'scale',
travel: 'plane',
pets: 'paw-print',
warranty: 'receipt',
taxes: 'file-spreadsheet',
work: 'briefcase-business',
other: 'folder',
};
function categoryLabels() {
return Object.fromEntries(CATEGORIES.map((category) => [category, t(`documents.category.${category}`)]));
}
let state = {
documents: [],
members: [],
view: localStorage.getItem('oikos-documents-view') || 'grid',
status: 'active',
category: '',
query: '',
};
let _container = null;
export async function render(container) {
_container = container;
container.innerHTML = `
<div class="documents-page">
<div class="documents-toolbar">
<h1 class="documents-toolbar__title">${t('documents.title')}</h1>
<div class="documents-toolbar__search">
<i data-lucide="search" class="documents-toolbar__search-icon" aria-hidden="true"></i>
<input class="documents-toolbar__search-input" id="documents-search" type="search" placeholder="${t('documents.searchPlaceholder')}" autocomplete="off">
</div>
<div class="documents-view-toggle" role="group" aria-label="${t('documents.viewToggle')}">
<button class="documents-view-toggle__btn ${state.view === 'grid' ? 'documents-view-toggle__btn--active' : ''}" data-view="grid" aria-label="${t('documents.gridView')}">
<i data-lucide="layout-grid" aria-hidden="true"></i>
</button>
<button class="documents-view-toggle__btn ${state.view === 'list' ? 'documents-view-toggle__btn--active' : ''}" data-view="list" aria-label="${t('documents.listView')}">
<i data-lucide="list" aria-hidden="true"></i>
</button>
</div>
<button class="btn btn--primary" id="documents-add-btn">
<i data-lucide="upload" class="icon-base" aria-hidden="true"></i>
${t('documents.addButton')}
</button>
</div>
<div class="documents-filters">
<select class="input documents-filter-select" id="documents-status">
<option value="active">${t('documents.statusActive')}</option>
<option value="archived">${t('documents.statusArchived')}</option>
</select>
<select class="input documents-filter-select" id="documents-category">
<option value="">${t('documents.allCategories')}</option>
${CATEGORIES.map((category) => `<option value="${category}">${categoryLabels()[category]}</option>`).join('')}
</select>
</div>
<div id="documents-list" class="documents-list documents-list--${state.view}"></div>
<button class="page-fab" id="fab-new-document" aria-label="${t('documents.addButton')}">
<i data-lucide="upload" class="icon-2xl" aria-hidden="true"></i>
</button>
</div>
`;
if (window.lucide) lucide.createIcons();
await Promise.all([loadMembers(), loadDocuments()]);
bindPageEvents();
renderDocuments();
}
async function loadMembers() {
const res = await api.get('/family/members');
state.members = res.data || [];
}
async function loadDocuments() {
const params = new URLSearchParams();
params.set('status', state.status);
if (state.category) params.set('category', state.category);
const res = await api.get(`/documents?${params.toString()}`);
state.documents = res.data || [];
}
function bindPageEvents() {
_container.querySelector('#documents-add-btn')?.addEventListener('click', () => openDocumentModal());
_container.querySelector('#fab-new-document')?.addEventListener('click', () => openDocumentModal());
_container.querySelector('#documents-search')?.addEventListener('input', (e) => {
state.query = e.target.value.trim().toLowerCase();
renderDocuments();
});
_container.querySelector('#documents-status')?.addEventListener('change', async (e) => {
state.status = e.target.value;
await loadDocuments();
renderDocuments();
});
_container.querySelector('#documents-category')?.addEventListener('change', async (e) => {
state.category = e.target.value;
await loadDocuments();
renderDocuments();
});
_container.querySelector('.documents-view-toggle')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-view]');
if (!btn) return;
state.view = btn.dataset.view;
localStorage.setItem('oikos-documents-view', state.view);
_container.querySelectorAll('.documents-view-toggle__btn').forEach((el) =>
el.classList.toggle('documents-view-toggle__btn--active', el === btn)
);
renderDocuments();
});
_container.querySelector('#documents-list')?.addEventListener('click', handleDocumentAction);
}
function filteredDocuments() {
if (!state.query) return state.documents;
return state.documents.filter((doc) =>
doc.name.toLowerCase().includes(state.query) ||
(doc.description || '').toLowerCase().includes(state.query) ||
doc.original_name.toLowerCase().includes(state.query)
);
}
function renderDocuments() {
const list = _container.querySelector('#documents-list');
if (!list) return;
const docs = filteredDocuments();
list.className = `documents-list documents-list--${state.view}`;
if (!docs.length) {
list.innerHTML = `
<div class="empty-state">
<i data-lucide="folder-open" class="empty-state__icon" aria-hidden="true"></i>
<div class="empty-state__title">${t('documents.emptyTitle')}</div>
<div class="empty-state__description">${t('documents.emptyDescription')}</div>
</div>
`;
if (window.lucide) lucide.createIcons();
return;
}
list.innerHTML = docs.map((doc) => state.view === 'list' ? renderListItem(doc) : renderGridCard(doc)).join('');
if (window.lucide) lucide.createIcons();
stagger(list.querySelectorAll('.document-card, .document-row'));
}
function renderMeta(doc) {
const labels = categoryLabels();
return `
<span><i data-lucide="${CATEGORY_ICONS[doc.category] || 'folder'}" aria-hidden="true"></i>${labels[doc.category] || doc.category}</span>
<span><i data-lucide="${doc.visibility === 'family' ? 'users' : doc.visibility === 'private' ? 'lock' : 'user-check'}" aria-hidden="true"></i>${t(`documents.visibility.${doc.visibility}`)}</span>
<span>${formatFileSize(doc.file_size)}</span>
`;
}
function renderActions(doc) {
return `
<a class="btn btn--ghost btn--icon btn--icon-sm" href="/api/v1/documents/${doc.id}/download" download title="${t('documents.downloadAction')}" aria-label="${t('documents.downloadAction')}">
<i data-lucide="download" class="icon-base" aria-hidden="true"></i>
</a>
<button class="btn btn--ghost btn--icon btn--icon-sm" data-action="edit" data-id="${doc.id}" title="${t('documents.editAction')}" aria-label="${t('documents.editAction')}">
<i data-lucide="settings" class="icon-base" aria-hidden="true"></i>
</button>
<button class="btn btn--ghost btn--icon btn--icon-sm" data-action="archive" data-id="${doc.id}" data-archived="${doc.status === 'archived'}" title="${doc.status === 'archived' ? t('documents.restoreAction') : t('documents.archiveAction')}" aria-label="${doc.status === 'archived' ? t('documents.restoreAction') : t('documents.archiveAction')}">
<i data-lucide="${doc.status === 'archived' ? 'archive-restore' : 'archive'}" class="icon-base" aria-hidden="true"></i>
</button>
<button class="btn btn--ghost btn--icon btn--icon-sm documents-danger" data-action="delete" data-id="${doc.id}" title="${t('common.delete')}" aria-label="${t('common.delete')}">
<i data-lucide="trash-2" class="icon-base" aria-hidden="true"></i>
</button>
`;
}
function renderGridCard(doc) {
return `
<article class="document-card" data-id="${doc.id}">
<div class="document-card__icon"><i data-lucide="${CATEGORY_ICONS[doc.category] || 'file'}" aria-hidden="true"></i></div>
<div class="document-card__body">
<h2 class="document-card__title">${esc(doc.name)}</h2>
<p class="document-card__description">${esc(doc.description || doc.original_name)}</p>
<div class="document-card__meta">${renderMeta(doc)}</div>
</div>
<div class="document-card__footer">
<span>${formatDate(doc.updated_at)}</span>
<div class="document-card__actions">${renderActions(doc)}</div>
</div>
</article>
`;
}
function renderListItem(doc) {
return `
<article class="document-row" data-id="${doc.id}">
<div class="document-row__icon"><i data-lucide="${CATEGORY_ICONS[doc.category] || 'file'}" aria-hidden="true"></i></div>
<div class="document-row__body">
<h2 class="document-row__title">${esc(doc.name)}</h2>
<div class="document-row__meta">${renderMeta(doc)}</div>
</div>
<div class="document-row__actions">${renderActions(doc)}</div>
</article>
`;
}
async function handleDocumentAction(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const doc = state.documents.find((item) => String(item.id) === String(btn.dataset.id));
if (!doc) return;
if (btn.dataset.action === 'edit') openDocumentModal(doc);
if (btn.dataset.action === 'archive') {
await api.patch(`/documents/${doc.id}/archive`, { archived: doc.status !== 'archived' });
window.oikos?.showToast(doc.status === 'archived' ? t('documents.restoredToast') : t('documents.archivedToast'), 'success');
await loadDocuments();
renderDocuments();
}
if (btn.dataset.action === 'delete') {
if (!confirm(t('documents.deleteConfirm', { name: doc.name }))) return;
await api.delete(`/documents/${doc.id}`);
window.oikos?.showToast(t('documents.deletedToast'), 'success');
await loadDocuments();
renderDocuments();
}
}
function memberOptions(selected = []) {
const selectedSet = new Set(selected.map(String));
return state.members.map((member) => `
<label class="document-member-option">
<input type="checkbox" value="${member.id}" ${selectedSet.has(String(member.id)) ? 'checked' : ''}>
<span>${esc(member.display_name)}</span>
</label>
`).join('');
}
function openDocumentModal(doc = null) {
const isEdit = !!doc;
openSharedModal({
title: isEdit ? t('documents.editTitle') : t('documents.newTitle'),
size: 'lg',
content: `
<form id="document-form" class="document-form">
<div class="modal-grid modal-grid--2">
<div class="form-group">
<label class="label" for="document-name">${t('documents.nameLabel')}</label>
<input class="input" id="document-name" name="name" required maxlength="200" value="${esc(doc?.name || '')}">
</div>
<div class="form-group">
<label class="label" for="document-category">${t('documents.categoryLabel')}</label>
<select class="input" id="document-category">
${CATEGORIES.map((category) => `<option value="${category}" ${doc?.category === category ? 'selected' : ''}>${categoryLabels()[category]}</option>`).join('')}
</select>
</div>
</div>
<div class="form-group">
<label class="label" for="document-description">${t('documents.descriptionLabel')}</label>
<textarea class="input" id="document-description" rows="3" maxlength="5000">${esc(doc?.description || '')}</textarea>
</div>
${!isEdit ? `
<div class="form-group">
<label class="label" for="document-file">${t('documents.fileLabel')}</label>
<label class="document-dropzone" id="document-dropzone" for="document-file">
<input class="sr-only" id="document-file" type="file" required>
<span class="document-dropzone__icon">
<i data-lucide="file-up" aria-hidden="true"></i>
</span>
<span class="document-dropzone__title">${t('documents.dropzoneTitle')}</span>
<span class="document-dropzone__hint">${t('documents.dropzoneHint')}</span>
<span class="document-dropzone__file" id="document-selected-file" hidden></span>
</label>
<p class="document-form__hint">${t('documents.fileHint')}</p>
</div>` : ''}
<div class="modal-grid modal-grid--2">
<div class="form-group">
<label class="label" for="document-visibility">${t('documents.visibilityLabel')}</label>
<select class="input" id="document-visibility">
<option value="family" ${doc?.visibility === 'family' ? 'selected' : ''}>${t('documents.visibility.family')}</option>
<option value="restricted" ${doc?.visibility === 'restricted' ? 'selected' : ''}>${t('documents.visibility.restricted')}</option>
<option value="private" ${doc?.visibility === 'private' ? 'selected' : ''}>${t('documents.visibility.private')}</option>
</select>
</div>
<div class="form-group">
<label class="label" for="document-status">${t('documents.statusLabel')}</label>
<select class="input" id="document-status">
<option value="active" ${doc?.status !== 'archived' ? 'selected' : ''}>${t('documents.statusActive')}</option>
<option value="archived" ${doc?.status === 'archived' ? 'selected' : ''}>${t('documents.statusArchived')}</option>
</select>
</div>
</div>
<div class="document-member-picker" id="document-member-picker">
<div class="label">${t('documents.allowedMembersLabel')}</div>
<div class="document-member-picker__grid">${memberOptions(doc?.allowed_member_ids || [])}</div>
</div>
<div id="document-error" class="login-error" hidden></div>
<div class="modal-panel__footer" style="padding:0;border:none;margin-top:var(--space-5)">
<button type="submit" class="btn btn--primary" id="document-submit">${isEdit ? t('common.save') : t('documents.uploadAction')}</button>
</div>
</form>
`,
onSave(panel) {
const form = panel.querySelector('#document-form');
const visibility = panel.querySelector('#document-visibility');
const picker = panel.querySelector('#document-member-picker');
const syncVisibility = () => { picker.hidden = visibility.value !== 'restricted'; };
visibility.addEventListener('change', syncVisibility);
syncVisibility();
bindDropzone(panel);
form.addEventListener('submit', (event) => saveDocument(event, doc));
},
});
}
function bindDropzone(panel) {
const dropzone = panel.querySelector('#document-dropzone');
const input = panel.querySelector('#document-file');
const selected = panel.querySelector('#document-selected-file');
if (!dropzone || !input || !selected) return;
const syncSelectedFile = () => {
const file = input.files?.[0];
selected.hidden = !file;
selected.textContent = file ? t('documents.selectedFileLabel', { name: file.name }) : '';
};
input.addEventListener('change', syncSelectedFile);
['dragenter', 'dragover'].forEach((eventName) => {
dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
dropzone.classList.add('document-dropzone--active');
});
});
['dragleave', 'drop'].forEach((eventName) => {
dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
dropzone.classList.remove('document-dropzone--active');
});
});
dropzone.addEventListener('drop', (event) => {
const file = event.dataTransfer?.files?.[0];
if (!file) return;
const transfer = new DataTransfer();
transfer.items.add(file);
input.files = transfer.files;
syncSelectedFile();
});
}
async function saveDocument(event, doc) {
event.preventDefault();
const form = event.target;
const error = form.querySelector('#document-error');
const submit = form.querySelector('#document-submit');
error.hidden = true;
submit.disabled = true;
try {
const visibility = form.querySelector('#document-visibility').value;
const payload = {
name: form.querySelector('#document-name').value.trim(),
description: form.querySelector('#document-description').value.trim() || null,
category: form.querySelector('#document-category').value,
visibility,
status: form.querySelector('#document-status').value,
allowed_member_ids: visibility === 'restricted'
? Array.from(form.querySelectorAll('.document-member-picker input:checked')).map((input) => Number(input.value))
: [],
};
if (!doc) {
const file = form.querySelector('#document-file').files?.[0];
if (!file) throw new Error(t('documents.fileRequired'));
if (file.size > MAX_FILE_SIZE) throw new Error(t('documents.fileTooLarge'));
payload.original_name = file.name;
payload.content_data = await readFileAsDataUrl(file);
if (!payload.name) payload.name = file.name.replace(/\.[^.]+$/, '');
}
if (!payload.name) throw new Error(t('common.required'));
if (doc) await api.put(`/documents/${doc.id}`, payload);
else await api.post('/documents', payload);
window.oikos?.showToast(doc ? t('documents.savedToast') : t('documents.uploadedToast'), 'success');
closeModal({ force: true });
await loadDocuments();
renderDocuments();
} catch (err) {
error.textContent = err.message;
error.hidden = false;
} finally {
submit.disabled = false;
}
}
function readFileAsDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error(t('documents.fileReadError')));
reader.readAsDataURL(file);
});
}
function formatFileSize(bytes) {
if (!bytes) return '0 KB';
if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
+4 -1
View File
@@ -25,6 +25,7 @@ const ROUTES = [
{ path: '/recipes', page: '/pages/recipes.js', requiresAuth: true, module: 'recipes' },
{ path: '/contacts', page: '/pages/contacts.js', requiresAuth: true, module: 'contacts' },
{ path: '/budget', page: '/pages/budget.js', requiresAuth: true, module: 'budget' },
{ path: '/documents', page: '/pages/documents.js', requiresAuth: true, module: 'documents' },
{ path: '/settings', page: '/pages/settings.js', requiresAuth: true, module: 'settings' },
];
@@ -128,7 +129,7 @@ let _pendingLoginRedirect = false;
// --------------------------------------------------------
const ROUTE_ORDER = ['/', '/tasks', '/calendar', '/birthdays', '/meals', '/recipes', '/shopping',
'/notes', '/contacts', '/budget', '/settings'];
'/notes', '/contacts', '/budget', '/documents', '/settings'];
const PRIMARY_NAV = 4;
@@ -181,6 +182,7 @@ function routeTitle(path) {
'/notes': t('nav.notes'),
'/contacts': t('nav.contacts'),
'/budget': t('nav.budget'),
'/documents': t('nav.documents'),
'/settings': t('nav.settings'),
};
return map[path] || getAppName();
@@ -886,6 +888,7 @@ function navItems() {
{ path: '/notes', label: t('nav.notes'), icon: 'sticky-note' },
{ path: '/contacts', label: t('nav.contacts'), icon: 'book-user' },
{ path: '/budget', label: t('nav.budget'), icon: 'wallet' },
{ path: '/documents', label: t('nav.documents'), icon: 'folder-lock' },
{ path: '/settings', label: t('nav.settings'), icon: 'settings' },
];
}
+346
View File
@@ -0,0 +1,346 @@
/**
* Module: Family Documents
* Purpose: Documents page layout, cards, list rows and upload modal controls.
*/
.documents-page {
--module-accent: var(--module-documents);
max-width: var(--content-max-width);
margin: 0 auto;
}
.documents-toolbar {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-top: 3px solid var(--module-accent);
border-bottom: 1px solid var(--color-border);
background-color: var(--color-surface);
}
.documents-toolbar__title {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
flex: 0 0 auto;
}
.documents-toolbar__search {
position: relative;
flex: 1;
min-width: 180px;
}
.documents-toolbar__search-icon {
position: absolute;
left: var(--space-4);
top: 50%;
width: 18px;
height: 18px;
transform: translateY(-50%);
color: var(--color-text-tertiary);
pointer-events: none;
}
.documents-toolbar__search-input {
width: 100%;
min-height: var(--target-base);
padding: 0 var(--space-3) 0 calc(var(--space-10) + var(--space-1));
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-2);
color: var(--color-text-primary);
}
.documents-view-toggle {
display: inline-flex;
padding: 2px;
gap: 2px;
border-radius: var(--radius-md);
background: var(--color-surface-2);
}
.documents-view-toggle__btn {
width: var(--target-base);
height: var(--target-base);
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: calc(var(--radius-md) - 2px);
color: var(--color-text-secondary);
}
.documents-view-toggle__btn svg {
width: 17px;
height: 17px;
}
.documents-view-toggle__btn--active {
color: var(--module-accent);
background: var(--color-surface);
box-shadow: var(--shadow-sm);
}
.documents-filters {
display: flex;
gap: var(--space-3);
padding: var(--space-4);
}
.documents-filter-select {
max-width: 240px;
}
.documents-list {
padding: 0 var(--space-4) calc(var(--nav-bottom-height) + var(--space-8));
}
.documents-list--grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-4);
}
.documents-list--list {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.document-card,
.document-row {
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
background: var(--color-surface);
box-shadow: var(--shadow-sm);
}
.document-card {
min-height: 230px;
display: flex;
flex-direction: column;
padding: var(--space-4);
}
.document-card__icon,
.document-row__icon {
width: 42px;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
color: var(--module-accent);
background: color-mix(in srgb, var(--module-accent) 12%, transparent);
}
.document-card__icon svg,
.document-row__icon svg {
width: 22px;
height: 22px;
}
.document-card__body {
flex: 1;
margin-top: var(--space-4);
}
.document-card__title,
.document-row__title {
margin: 0;
font-size: var(--text-base);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
overflow-wrap: anywhere;
}
.document-card__description {
min-height: 42px;
margin: var(--space-2) 0 0;
color: var(--color-text-secondary);
font-size: var(--text-sm);
overflow-wrap: anywhere;
}
.document-card__meta,
.document-row__meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-3);
color: var(--color-text-tertiary);
font-size: var(--text-xs);
}
.document-card__meta span,
.document-row__meta span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.document-card__meta svg,
.document-row__meta svg {
width: 13px;
height: 13px;
}
.document-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
margin-top: var(--space-4);
color: var(--color-text-tertiary);
font-size: var(--text-xs);
}
.document-card__actions,
.document-row__actions {
display: flex;
align-items: center;
gap: var(--space-1);
}
.document-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
}
.document-row__body {
min-width: 0;
}
.documents-danger {
color: var(--color-danger);
}
.document-form__hint {
margin-top: var(--space-1);
color: var(--color-text-tertiary);
font-size: var(--text-xs);
}
.document-dropzone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
min-height: 148px;
padding: var(--space-5);
border: 1.5px dashed color-mix(in srgb, var(--module-accent) 48%, var(--color-border));
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--module-accent) 7%, var(--color-surface));
color: var(--color-text-secondary);
text-align: center;
cursor: pointer;
transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast);
}
.document-dropzone:hover,
.document-dropzone--active {
border-color: var(--module-accent);
background: color-mix(in srgb, var(--module-accent) 12%, var(--color-surface));
}
.document-dropzone--active {
transform: translateY(-1px);
}
.document-dropzone__icon {
width: 42px;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
color: var(--module-accent);
background: var(--color-surface);
box-shadow: var(--shadow-sm);
}
.document-dropzone__icon svg {
width: 22px;
height: 22px;
}
.document-dropzone__title {
font-size: var(--text-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.document-dropzone__hint,
.document-dropzone__file {
max-width: 100%;
font-size: var(--text-xs);
overflow-wrap: anywhere;
}
.document-dropzone__file {
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
color: var(--module-accent);
background: color-mix(in srgb, var(--module-accent) 14%, transparent);
font-weight: var(--font-weight-semibold);
}
.document-member-picker {
margin-top: var(--space-2);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-3);
background: var(--color-surface-2);
}
.document-member-picker__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: var(--space-2);
margin-top: var(--space-2);
}
.document-member-option {
display: flex;
align-items: center;
gap: var(--space-2);
min-height: var(--target-sm);
color: var(--color-text-secondary);
font-size: var(--text-sm);
}
@media (max-width: 720px) {
.documents-toolbar {
flex-wrap: wrap;
}
.documents-toolbar__title {
width: 100%;
}
.documents-toolbar__search {
order: 4;
flex-basis: 100%;
}
.documents-filters {
flex-direction: column;
}
.documents-filter-select {
max-width: none;
}
.document-row {
grid-template-columns: auto minmax(0, 1fr);
}
.document-row__actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
}
+2
View File
@@ -172,6 +172,8 @@
--module-birthdays: var(--_module-birthdays); /* Rose - Geburtstage */
--_module-budget: #0F766E;
--module-budget: var(--_module-budget); /* Teal-700 - Finanzen, Stabilität */
--_module-documents: #1D4ED8;
--module-documents: var(--_module-documents); /* Blue - secure family documents */
--_module-settings: #6E7781;
--module-settings: var(--_module-settings); /* Grau - Konfiguration */
--_module-reminders: #0E7490;
+6 -4
View File
@@ -13,10 +13,10 @@
* → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit)
*/
const SHELL_CACHE = 'oikos-shell-v66';
const PAGES_CACHE = 'oikos-pages-v61';
const LOCALES_CACHE = 'oikos-locales-v10';
const ASSETS_CACHE = 'oikos-assets-v61';
const SHELL_CACHE = 'oikos-shell-v68';
const PAGES_CACHE = 'oikos-pages-v63';
const LOCALES_CACHE = 'oikos-locales-v12';
const ASSETS_CACHE = 'oikos-assets-v63';
const BYPASS_CACHE = 'oikos-bypass-flag';
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, LOCALES_CACHE, ASSETS_CACHE];
@@ -47,6 +47,7 @@ const APP_SHELL = [
'/styles/contacts.css',
'/styles/birthdays.css',
'/styles/budget.css',
'/styles/documents.css',
'/styles/settings.css',
'/styles/recipes.css',
'/components/oikos-install-prompt.js',
@@ -90,6 +91,7 @@ const PAGE_MODULES = [
'/pages/contacts.js',
'/pages/birthdays.js',
'/pages/budget.js',
'/pages/documents.js',
'/pages/settings.js',
'/pages/login.js',
'/pages/recipes.js',