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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ props.row.clazz }} 起草議長函
+
+
+ {{ props.row.clazz }} 起草訴狀
+
+
+
+
+
+
+
+
+
+ {{ dialogTitle }}
+
+
+
+
+ 已生成文件內容,點擊「起草」按鈕將自動複製並開啟填寫頁面
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 複製提案附件
+
+
+ {{ props.row.done ? '標記為進行中' : '標記為已完成' }}
+
+
+ 刪除
+
+
+
+
+
+
+
+
+
+ 確認刪除
+
+ 確定要刪除提案「{{ proposalToDelete?.title }}」嗎?
+
+
+
+
+
+
+
+
+
+
+
+
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;
+}