Integrated application chat system

This commit is contained in:
2025-08-26 13:38:17 -04:00
parent 342c6cd706
commit e43117b64f
6 changed files with 174 additions and 76 deletions

View File

@@ -44,35 +44,42 @@ app.post('/application', async (req, res) => {
});
app.get('/application/me', async (req, res) => {
try {
// TODO: replace with current user ID
const applicationId = 1;
// app.get('/application/me', async (req, res) => {
// try {
// // TODO: replace with current user ID
// const applicationId = 1;
const rows = await pool.query(
`SELECT app.*,
member.name AS member_name
FROM applications AS app
INNER JOIN members AS member ON member.id = app.member_id
WHERE app.member_id = ?;`,
[applicationId]
);
// const rows = await pool.query(
// `SELECT app.*,
// member.name AS member_name
// FROM applications AS app
// INNER JOIN members AS member ON member.id = app.member_id
// WHERE app.member_id = ?;`,
// [applicationId]
// );
if (!Array.isArray(rows) || rows.length === 0) {
return res.sendStatus(204);
}
// if (!Array.isArray(rows) || rows.length === 0) {
// return res.sendStatus(204);
// }
return res.status(200).json(rows[0]);
} catch (err) {
console.error('Query failed:', err);
return res.status(500).json({ error: 'Failed to load application' });
}
});
// return res.status(200).json(rows[0]);
// } catch (err) {
// console.error('Query failed:', err);
// return res.status(500).json({ error: 'Failed to load application' });
// }
// });
app.get('/application/:id', async (req, res) => {
const appID = req.params.id;
let appID = req.params.id;
//TODO: Replace with real user Authorization and whatnot
if (appID === "me")
appID = 1;
try {
const rows = await pool.query(
const conn = await pool.getConnection()
const application = await conn.query(
`SELECT app.*,
member.name AS member_name
FROM applications AS app
@@ -81,11 +88,29 @@ app.get('/application/:id', async (req, res) => {
[appID]
);
if (!Array.isArray(rows) || rows.length === 0) {
return res.send(404).json("Application Not Found");
if (!Array.isArray(application) || application.length === 0) {
conn.release();
return res.status(204).json("Application Not Found");
}
return res.status(200).json(rows[0]);
const comments = await conn.query(`SELECT app.id AS comment_id,
app.post_content,
app.poster_id,
app.post_time,
app.last_modified,
member.name AS poster_name
FROM application_comments AS app
INNER JOIN members AS member ON member.id = app.poster_id
WHERE app.application_id = ?;`,
[appID]);
conn.release()
const output = {
application: application[0],
comments,
}
return res.status(200).json(output);
}
catch (err) {
console.error('Query failed:', err);
@@ -115,12 +140,6 @@ app.get('/application/all', async (req, res) => {
}
});
app.post('/application/message', (req, res) => {
const data = req.body;
applicationData.messages.push(data);
res.status(200).send();
});
app.post('/application/approve/:id', async (req, res) => {
const appID = req.params.id;
@@ -179,6 +198,46 @@ app.post('/application/deny/:id', async (req, res) => {
}
});
app.post('/application/:id/comment', async (req, res) => {
const appID = req.params.id;
const data = req.body.message;
const user = 1;
const sql = `INSERT INTO application_comments(
application_id,
poster_id,
post_content
)
VALUES(?, ?, ?);`
try {
const conn = await pool.getConnection();
const result = await conn.query(sql, [appID, user, data])
console.log(result)
if (result.affectedRows !== 1) {
conn.release();
throw new Error("Insert Failure")
}
const getSQL = `SELECT app.id AS comment_id,
app.post_content,
app.poster_id,
app.post_time,
app.last_modified,
member.name AS poster_name
FROM application_comments AS app
INNER JOIN members AS member ON member.id = app.poster_id
WHERE app.id = ?; `;
const comment = await conn.query(getSQL, [result.insertId])
res.status(201).json(comment[0]);
} catch (err) {
console.error('Comment failed:', err);
res.status(500).json({ error: 'Could not post comment' });
}
})
app.listen(port, () => {
console.log(`Example app listening on port ${port} `)
})

View File

@@ -17,7 +17,7 @@ import ManageApplications from './pages/ManageApplications.vue';
</div>
<Separator></Separator>
<Application></Application>
<ManageApplications></ManageApplications>
<!-- <ManageApplications></ManageApplications> -->
<!-- <AutoForm class="max-w-3xl mx-auto my-20"></AutoForm> -->
</div>
</template>

View File

@@ -49,14 +49,24 @@ export interface ApplicationRow {
// present when you join members (e.g., SELECT a.*, m.name AS member_name)
member_name: string;
}
export interface CommentRow {
comment_id: number;
post_content: string;
poster_id: number;
post_time: string;
last_modified: string | null;
poster_name: string;
}
export interface ApplicationFull {
application: ApplicationRow;
comments: CommentRow[];
}
export type ApplicationFull = ApplicationRow & {
messages?: object[];
};
export enum Status {
Pending = "Pending",
Approved = "Approved",
Accepted = "Accepted",
Denied = "Denied",
}
@@ -83,19 +93,19 @@ export async function postApplication(val: any) {
})
}
export async function postChatMessage(val: any) {
export async function postChatMessage(message: any, post_id: number) {
let output = {
message: val,
sender: 1,
timestamp: Date.now(),
const out = {
message: message
}
await fetch(`http://${addr}/application/message`, {
const response = await fetch(`http://${addr}/application/${post_id}/comment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(output),
body: JSON.stringify(out),
})
return await response.json();
}
export async function getAllApplications() {

View File

@@ -40,12 +40,8 @@ function onSubmit(values: { text: string }, { resetForm }: { resetForm: () => vo
<FormItem>
<FormLabel class="sr-only">Comment</FormLabel>
<FormControl>
<Textarea
v-bind="componentField"
rows="3"
placeholder="Write a comment…"
class="bg-neutral-800 resize-none w-full"
/>
<Textarea v-bind="componentField" rows="3" placeholder="Write a comment…"
class="bg-neutral-800 resize-none w-full" />
</FormControl>
<FormMessage />
</FormItem>
@@ -59,12 +55,20 @@ function onSubmit(values: { text: string }, { resetForm }: { resetForm: () => vo
<!-- Existing posts -->
<div class="space-y-3">
<div
v-for="(m, i) in props.messages"
:key="m.id ?? i"
class="rounded-md border border-neutral-800 p-3"
>
<p class="text-sm whitespace-pre-wrap">{{ m.text ?? m.message ?? '' }}</p>
<div v-for="(message, i) in props.messages" :key="message.id ?? i"
class="rounded-md border border-neutral-800 p-3 space-y-5">
<!-- Comment header -->
<div class="flex justify-between">
<p>{{ message.poster_name }}</p>
<p class="text-muted-foreground">{{ new Date(message.post_time).toLocaleString("EN-us", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }}</p>
</div>
<p>{{ message.post_content }}</p>
</div>
</div>
</div>

View File

@@ -5,9 +5,10 @@ import { onMounted, ref } from 'vue';
import { ApplicationData, loadApplication, postApplication, postChatMessage, Status } from '@/api/application';
const appData = ref<ApplicationData>(null);
const appID = ref<number | null>(null);
const chatData = ref<object[]>([])
const readOnly = ref<boolean>(false);
const newApp = ref<boolean>(true);
const newApp = ref<boolean>(null);
const status = ref<Status>(null);
const decisionDate = ref<Date | null>(null);
const submitDate = ref<Date | null>(null);
@@ -15,19 +16,25 @@ const loading = ref<boolean>(true);
const member_name = ref<string>();
onMounted(async () => {
try {
const data = await loadApplication()
console.log(data);
if (data) {
const raw = await loadApplication()
if (raw === null) {
//new app
appData.value = null
readOnly.value = false;
newApp.value = true;
} else {
//load app
const data = raw.application;
appID.value = data.id;
appData.value = data.app_data;
chatData.value = data.messages;
chatData.value = raw.comments;
status.value = data.app_status;
decisionDate.value = new Date(data.decision_at);
submitDate.value = data.decision_at ? new Date(data.submitted_at) : null;
submitDate.value = data.submitted_at ? new Date(data.submitted_at) : null;
member_name.value = data.member_name;
readOnly.value = true;
} else {
appData.value = null
newApp.value = false;
readOnly.value = true;
}
} catch (e) {
console.error(e)
@@ -35,34 +42,52 @@ onMounted(async () => {
loading.value = false;
})
async function postComment(comment) {
chatData.value.push(await postChatMessage(comment, appID.value));
}
</script>
<template>
<div v-if="!loading" class="max-w-3xl mx-auto my-20">
<div v-if="newApp" class="flex flex-row justify-between items-center py-2 mb-8">
<div v-if="!newApp" class="flex flex-row justify-between items-center py-2 mb-8">
<!-- Application header -->
<div>
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">{{ member_name }}</h3>
<p class="text-muted-foreground">Submitted: {{ submitDate.toLocaleString("en-US") }}</p>
<p class="text-muted-foreground">Submitted: {{ submitDate.toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }}</p>
</div>
<div>
<h3 class="text-right" :class="[
'font-semibold',
status === Status.Pending && 'text-yellow-500',
status === Status.Approved && 'text-green-500',
status === Status.Accepted && 'text-green-500',
status === Status.Denied && 'text-red-500'
]">{{ status }}</h3>
<p class="text-muted-foreground">{{ status }}: {{ decisionDate.toLocaleString("en-US") }}</p>
<p v-if="status != Status.Pending" class="text-muted-foreground">{{ status }}: {{
decisionDate.toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }}</p>
</div>
</div>
<div v-else class="flex flex-row justify-between items-center py-2 mb-8">
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">Apply to join the 17th Rangers</h3>
</div>
<ApplicationForm v-if="appData" :read-only="readOnly" :data="appData" @submit="(e) => { postApplication(e) }"
class="mb-7"></ApplicationForm>
<div v-if="newApp">
<ApplicationForm :read-only="readOnly" :data="appData" @submit="(e) => { postApplication(e) }" class="mb-7">
</ApplicationForm>
<div v-if="!newApp">
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-4">Discussion</h3>
<ApplicationChat :messages="chatData" @post="postChatMessage"></ApplicationChat>
<ApplicationChat :messages="chatData" @post="postComment"></ApplicationChat>
</div>
</div>
<div v-else>HELLOOO</div>
</template>

View File

@@ -63,7 +63,7 @@ onMounted(async () => {
})
</script>
<template>
<Table>
<Table class="mx-auto max-w-3xl">
<!-- <TableCaption>A list of your recent invoices.</TableCaption> -->
<TableHeader>
<TableRow>
@@ -81,7 +81,7 @@ onMounted(async () => {
<TableCell v-if="app.app_status != 'Pending'" class="text-right" :class="[
'font-semibold',
app.app_status === Status.Pending && 'text-yellow-500',
app.app_status === Status.Approved && 'text-success',
app.app_status === Status.Accepted && 'text-success',
app.app_status === Status.Denied && 'text-destructive'
]">{{ app.app_status }}</TableCell>
<TableCell v-else class="inline-flex items-end gap-2">