From 26d0e848f263c708b909692de3a0df31a3a6927c Mon Sep 17 00:00:00 2001 From: ChrisSun0731 Date: Thu, 27 Nov 2025 14:25:30 +0800 Subject: [PATCH] add export_indictment page --- src/layouts/MainLayout.vue | 11 + src/pages/mgmt/ManageMeetingsPage.vue | 13 +- src/pages/mgmt/attendance/AttendancePage.vue | 35 +- .../mgmt/attendance/ExportAttendancePage.vue | 7 +- .../mgmt/attendance/ExportIndictmentPage.vue | 362 ++++++++++++++++++ .../mgmt/attendance/ScheduledAbsencePage.vue | 1 + .../mgmt/attendance/SerialAbsencePage.vue | 3 +- src/pages/propose/ManageProposalPage.vue | 199 ++++++++++ src/pages/propose/ProposalPage.vue | 243 ++++++++++++ src/router/routes.ts | 11 + src/ts/auth.ts | 7 +- src/ts/models.ts | 1 - src/ts/proposalmodels.ts | 111 ++++++ 13 files changed, 979 insertions(+), 25 deletions(-) create mode 100644 src/pages/mgmt/attendance/ExportIndictmentPage.vue create mode 100644 src/pages/propose/ManageProposalPage.vue create mode 100644 src/pages/propose/ProposalPage.vue create mode 100644 src/ts/proposalmodels.ts diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 47a78d0..3bf192c 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -109,6 +109,12 @@ const endpoints = [ icon: 'groups', role: Role.Secretary, }, + /*{ + name: '提案管理', + url: '/manage_proposals', + icon: 'description', + role: Role.Secretary, + },*/ { name: '主持會議', url: '/meeting_host', @@ -126,6 +132,11 @@ const endpoints = [ icon: 'construction', role: Role.Secretary, }, + /*{ + name: '提案', + url: '/proposal', + icon: 'book', + },*/ { name: '關於', url: '/about', diff --git a/src/pages/mgmt/ManageMeetingsPage.vue b/src/pages/mgmt/ManageMeetingsPage.vue index 6078b66..127c694 100644 --- a/src/pages/mgmt/ManageMeetingsPage.vue +++ b/src/pages/mgmt/ManageMeetingsPage.vue @@ -114,7 +114,14 @@
- +

開會日期:

@@ -229,7 +236,7 @@ async function submit() { reign: targetMeeting.reign, registration: targetMeeting.registration, exemptFromAttendance: targetMeeting.exemptFromAttendance, - customAttendanceBar: (targetMeeting.customAttendanceBar) ? targetMeeting.customAttendanceBar : null, + customAttendanceBar: targetMeeting.customAttendanceBar ? targetMeeting.customAttendanceBar : null, punchInPasscode: targetMeeting.punchInPasscode, }); } else if (action.value === 'add') { @@ -247,7 +254,7 @@ async function submit() { reign: targetMeeting.reign, registration: targetMeeting.registration, exemptFromAttendance: targetMeeting.exemptFromAttendance, - customAttendanceBar: (targetMeeting.customAttendanceBar) ? targetMeeting.customAttendanceBar : null, + customAttendanceBar: targetMeeting.customAttendanceBar ? targetMeeting.customAttendanceBar : null, } as unknown as Meeting); } } catch (e) { diff --git a/src/pages/mgmt/attendance/AttendancePage.vue b/src/pages/mgmt/attendance/AttendancePage.vue index baf0992..f656191 100644 --- a/src/pages/mgmt/attendance/AttendancePage.vue +++ b/src/pages/mgmt/attendance/AttendancePage.vue @@ -3,6 +3,7 @@ + @@ -26,7 +27,7 @@ import type { User } from 'src/ts/models.ts'; import { meetingCollectionOfCurrentReign } from 'src/ts/models.ts'; import { ref, watch } from 'vue'; import type { QTableColumn } from 'quasar'; -import {currentReign, notifyError} from 'src/ts/utils.ts'; +import { currentReign, notifyError } from 'src/ts/utils.ts'; const accounts = ref(null as User[] | null); const meetings = meetingCollectionOfCurrentReign(); @@ -99,7 +100,7 @@ function updateAttendance() { } let meetingsCount = 0; for (const meeting of meetings.value) { - if (!meeting||meeting.exemptFromAttendance) continue; + if (!meeting || meeting.exemptFromAttendance) continue; for (const clazz of meeting.participants) { if (!tempAttendance[clazz]) continue; tempAttendance[clazz].attendedMeetings++; @@ -121,20 +122,22 @@ function updateAttendance() { attendance.value = Object.values(tempAttendance); } -getAllUsers().then((users) => { - accounts.value = users; - watch( - meetings, - () => { - updateAttendance(); - }, - { deep: true }, - ); - updateAttendance(); -}).catch(e => notifyError('載入資料失敗', e)) -.finally(()=> { - loading.value = false; -}); +getAllUsers() + .then((users) => { + accounts.value = users; + watch( + meetings, + () => { + updateAttendance(); + }, + { deep: true }, + ); + updateAttendance(); + }) + .catch((e) => notifyError('載入資料失敗', e)) + .finally(() => { + loading.value = false; + }); diff --git a/src/pages/mgmt/attendance/ExportAttendancePage.vue b/src/pages/mgmt/attendance/ExportAttendancePage.vue index 2a65b86..4a1e736 100644 --- a/src/pages/mgmt/attendance/ExportAttendancePage.vue +++ b/src/pages/mgmt/attendance/ExportAttendancePage.vue @@ -2,6 +2,7 @@ + @@ -16,7 +17,7 @@ import { rawMeetingsOfCurrentReignQuery } from 'src/ts/models.ts'; import ExcelJS from 'exceljs'; import { exportFile, Loading } from 'quasar'; import { getDocs } from 'firebase/firestore'; -import {cleanseName, notifyError} from 'src/ts/utils.ts'; +import { cleanseName, notifyError } from 'src/ts/utils.ts'; async function exp() { Loading.show(); @@ -38,12 +39,12 @@ async function exp() { if (!account.clazz) continue; let serviceHours = 0; for (const meeting of meetings) { - if (!meeting||meeting.data()?.exemptFromAttendance) continue; + if (!meeting || meeting.data()?.exemptFromAttendance) continue; if (meeting.data()?.participants.includes(account.clazz)) { serviceHours++; } } - const attRate = serviceHours / meetings.filter(f => f.data() && !f.data()!.exemptFromAttendance).length; + const attRate = serviceHours / meetings.filter((f) => f.data() && !f.data()!.exemptFromAttendance).length; let awardType; if (attRate == 1) { awardType = '小功乙支'; diff --git a/src/pages/mgmt/attendance/ExportIndictmentPage.vue b/src/pages/mgmt/attendance/ExportIndictmentPage.vue new file mode 100644 index 0000000..b69b022 --- /dev/null +++ b/src/pages/mgmt/attendance/ExportIndictmentPage.vue @@ -0,0 +1,362 @@ + + + + + diff --git a/src/pages/mgmt/attendance/ScheduledAbsencePage.vue b/src/pages/mgmt/attendance/ScheduledAbsencePage.vue index 0a8c468..c848d49 100644 --- a/src/pages/mgmt/attendance/ScheduledAbsencePage.vue +++ b/src/pages/mgmt/attendance/ScheduledAbsencePage.vue @@ -2,6 +2,7 @@ + diff --git a/src/pages/mgmt/attendance/SerialAbsencePage.vue b/src/pages/mgmt/attendance/SerialAbsencePage.vue index 41fc6d3..fdb3307 100644 --- a/src/pages/mgmt/attendance/SerialAbsencePage.vue +++ b/src/pages/mgmt/attendance/SerialAbsencePage.vue @@ -3,6 +3,7 @@ + @@ -42,7 +43,7 @@ function updateAttendance() { const participants = [] as string[][]; const scheduledAbsences = [] as string[][]; for (const meeting of meetings.value) { - if (!meeting||meeting.exemptFromAttendance) continue; + if (!meeting || meeting.exemptFromAttendance) continue; participants.push(meeting.participants); scheduledAbsences.push(Object.keys(meeting.absences)); } diff --git a/src/pages/propose/ManageProposalPage.vue b/src/pages/propose/ManageProposalPage.vue new file mode 100644 index 0000000..18316c4 --- /dev/null +++ b/src/pages/propose/ManageProposalPage.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/src/pages/propose/ProposalPage.vue b/src/pages/propose/ProposalPage.vue new file mode 100644 index 0000000..980cb1d --- /dev/null +++ b/src/pages/propose/ProposalPage.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/router/routes.ts b/src/router/routes.ts index db35e12..8edc687 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -70,6 +70,7 @@ const routes: RouteRecordRaw[] = [ { path: 'serial_absence', component: () => import('pages/mgmt/attendance/SerialAbsencePage.vue') }, { path: 'scheduled_absence', component: () => import('pages/mgmt/attendance/ScheduledAbsencePage.vue') }, { path: 'export', component: () => import('pages/mgmt/attendance/ExportAttendancePage.vue') }, + { path: 'export_indictment', component: () => import('pages/mgmt/attendance/ExportIndictmentPage.vue') }, ], }, { @@ -77,6 +78,16 @@ const routes: RouteRecordRaw[] = [ component: () => import('layouts/MainLayout.vue'), children: [{ path: '', component: () => import('pages/IndexPage.vue') }], }, + { + path: '/proposal', + component: () => import('layouts/MainLayout.vue'), + children: [{ path: '', component: () => import('pages/propose/ProposalPage.vue') }], + }, + { + path: '/manage_proposals', + component: () => import('layouts/MainLayout.vue'), + children: [{ path: '', component: () => import('pages/propose/ManageProposalPage.vue') }], + }, { path: '/about', component: () => import('layouts/MainLayout.vue'), diff --git a/src/ts/auth.ts b/src/ts/auth.ts index b0973d2..c12dda1 100644 --- a/src/ts/auth.ts +++ b/src/ts/auth.ts @@ -1,7 +1,7 @@ import { useFirebaseAuth } from 'vuefire'; import type { User } from 'firebase/auth'; import { browserLocalPersistence, GoogleAuthProvider, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'; -import type { Ref} from 'vue'; +import type { Ref } from 'vue'; import { reactive, ref } from 'vue'; import { Loading } from 'quasar'; import type * as models from 'src/ts/models.ts'; @@ -118,3 +118,8 @@ export function translateRole(role: number | undefined) { export async function getAllUsers(): Promise { return (await useFunction('getAllUsers')()).data as models.User[]; } + +const getAllProposalFunction = useFunction('getAllProposal'); +export async function getAllProposal(): Promise { + return (await getAllProposalFunction()).data as models.Proposal[]; +} diff --git a/src/ts/models.ts b/src/ts/models.ts index f905dd4..c7278c0 100644 --- a/src/ts/models.ts +++ b/src/ts/models.ts @@ -88,7 +88,6 @@ export function meetingCollectionOfReign(reign: Ref) { return useCollection(meetingsQuery); } - export function rawMeetingsOfCurrentReignQuery() { return query(query(rawMeetingCollection(), orderBy('start', 'desc')), where('reign', '==', currentReign)); } diff --git a/src/ts/proposalmodels.ts b/src/ts/proposalmodels.ts new file mode 100644 index 0000000..f0eeb16 --- /dev/null +++ b/src/ts/proposalmodels.ts @@ -0,0 +1,111 @@ +import { collection, doc, orderBy, query, Timestamp } from 'firebase/firestore'; +import { firestoreDefaultConverter, useCollection, useDocument, useFirestore } from 'vuefire'; +import type { FirestoreDataConverter } from '@firebase/firestore'; + +export interface Proposal { + title: string; + content: string; + type: string; + proposer: string; + reign: string; + basis?: string; + done?: boolean; + attachments?: string[]; + uploadedAt: Date; +} + +export interface ProposalId extends Proposal { + id: string; +} + +export const proposalConverter: FirestoreDataConverter = { + toFirestore(data: any) { + if (data.uploadedAt) { + data.uploadedAt = Timestamp.fromDate(data.uploadedAt); + } + return firestoreDefaultConverter.toFirestore(data); + }, + fromFirestore(snapshot, options) { + const data = firestoreDefaultConverter.fromFirestore(snapshot, options); + if (!data) return null; + if (data.uploadedAt) { + data.uploadedAt = new Date(data.uploadedAt.toMillis()); + } + return data as unknown as Proposal; + }, +}; + +// +export function rawUserProposalCollectionLaw(userId: string) { + const db = useFirestore(); + return collection(db, `proposal/law/${userId}/`).withConverter(proposalConverter); +} + +export function userProposalCollectionLaw(userId: string) { + return useCollection(query(rawUserProposalCollectionLaw(userId), orderBy('uploadedAt', 'desc'))); +} + +export function getProposalLaw(userId: string, proposalId: string) { + return useDocument(doc(rawUserProposalCollectionLaw(userId), proposalId)); +} + +// +export function rawUserProposalCollectionGeneral(userId: string) { + const db = useFirestore(); + return collection(db, `proposal/general/${userId}/`).withConverter(proposalConverter); +} + +export function userProposalCollectionGeneral(userId: string) { + return useCollection(query(rawUserProposalCollectionGeneral(userId), orderBy('uploadedAt', 'desc'))); +} + +export function getProposalGeneral(userId: string, proposalId: string) { + return useDocument(doc(rawUserProposalCollectionGeneral(userId), proposalId)); +} + +// +export function rawUserProposalCollectionPresentation(userId: string) { + const db = useFirestore(); + return collection(db, `proposal/presentation/${userId}/`).withConverter(proposalConverter); +} + +export function userProposalCollectionPresentation(userId: string) { + return useCollection(query(rawUserProposalCollectionPresentation(userId), orderBy('uploadedAt', 'desc'))); +} + +export function getProposal(userId: string, proposalId: string) { + return useDocument(doc(rawUserProposalCollectionPresentation(userId), proposalId)); +} + +export function generateProposalId(date: Date, index: number): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}${month}${day}_${index}`; +} + +export function parseProposalId(proposalId: string): { date: Date; index: number } | null { + const match = proposalId.match(/^(\d{4})(\d{2})(\d{2})_(\d+)$/); + if (!match) return null; + + const year = match[1]; + const month = match[2]; + const day = match[3]; + const index = match[4]; + + if (!year || !month || !day || !index) return null; + + return { + date: new Date(parseInt(year), parseInt(month) - 1, parseInt(day)), + index: parseInt(index), + }; +} + +export function translateProposalType(type: string): string { + const typeMap: Record = { + law: '法律修正案', + general: '一般提案', + presentation: '專案報告', + }; + return typeMap[type] || type; +}