Compare commits

...

7 commits

Author SHA1 Message Date
ed739e27fd /page/orders: create
All checks were successful
Build dev / build (push) Successful in 1m59s
this is pretty initial and the are still no per order page.
2023-12-18 23:17:06 +01:00
8bd41f8df3 create api/orders/count.get endpoint
this api returns count of all orders
2023-12-18 23:15:01 +01:00
b12e91ed13 Refactor entry editor and add some new types to it 2023-12-18 23:14:15 +01:00
0151a6c713 Update dependencies 2023-12-16 15:07:51 +01:00
5d1fc30601 add the newly created client to clients list 2023-12-12 19:18:13 +01:00
3101858eed check for undefined values when normalizing form new client data 2023-12-12 19:17:32 +01:00
f7519f32b3 nav: select automatically current page when visiting first time
for example after refresh, or when component gets loaded
2023-12-12 18:59:42 +01:00
10 changed files with 2884 additions and 4206 deletions

View file

@ -1,15 +1,25 @@
<script setup lang="ts">
import { ref } from "vue";
type optionalMap<Optional> = Optional extends true ? undefined : string | number;
// type typeMap<Type extends string = {};
import FormClient from "~/components/formClient.vue";
export type fieldDefinition<Optional extends boolean = boolean> = {
type optionalMap<Optional, type> = Optional extends true ? undefined | type : type;
type typeMap = {
"text": string,
"password": string,
"number": number,
"boolean": boolean,
"none": undefined,
"client": `${bigint}`
};
export type fieldDefinition<Optional extends boolean = boolean, type extends keyof typeMap = keyof typeMap> = {
key: string,
label?: string,
type: "text" | "password" | "number",
type: type,
optional?: Optional,
value?: optionalMap<Optional>,
value?: optionalMap<Optional, typeMap[type]>,
}
const props = defineProps<{
@ -22,7 +32,7 @@ const emit = defineEmits<{
(e: "updateSubModelValue", key: fieldDefinition["key"], value: fieldDefinition["value"]): void,
}>();
const modelValue = ref<{[key: string]: string | number | undefined}>({});
const modelValue = ref<{[key: string]: string | number | boolean | undefined}>({});
for (const i of props.fields) {
modelValue.value[i.key] = i.value;
@ -39,12 +49,43 @@ emit("update:modelValue", modelValue.value);
</script>
<template>
<div v-for="i of fields" :key="i.key">
<VTextField
v-for="i of fields"
:key="i.key"
v-if="i.type == 'text'"
:model-value="modelValue[i.key]"
:label="i.label"
:type="i.type"
type="text"
@update:model-value="v => updateModel(i.key, v)"
/>
<VTextField
v-if="i.type == 'password'"
:model-value="modelValue[i.key]"
:label="i.label"
type="password"
@update:model-value="v => updateModel(i.key, v)"
/>
<VTextField
v-if="i.type == 'number'"
:model-value="modelValue[i.key]"
:label="i.label"
type="number"
@update:model-value="v => updateModel(i.key, Number(v))"
/>
<v-checkbox
v-if="i.type == 'boolean'"
:label="i.label"
:model-value="modelValue[i.key]"
@update:model-value="v => updateModel(i.key, Boolean(v))"
/>
<p v-if="i.type == 'none'">
{{ i.label }}
</p>
<FormClient
v-if="i.type == 'client'"
:model-value="modelValue[i.key] as `${bigint}`"
:label="i.label"
:optional="i.optional"
@update:model-value="v => updateModel(i.key, v)"
/>
</div>
</template>

37
components/formClient.vue Normal file
View file

@ -0,0 +1,37 @@
<script setup lang="ts">
import { useFetch, createError } from '#app';
const props = defineProps<{
label?: string,
optional?: boolean,
modelValue?: `${bigint}`,
}>();
// eslint-disable-next-line func-call-spacing
const emit = defineEmits<{
(e: "update:modelValue", value: `${bigint}`): void,
}>();
// FIXME: allow to search all clients instead of newest 50 (needs api call)
const clientsRequest = await useFetch("/api/clients");
if (clientsRequest.error.value) throw createError(clientsRequest.error.value?.data ?? "");
const clients = clientsRequest.data.value?.map((e) => {
return {
value: e.id,
title: e.name ?? `[null] (${e.id})`,
props: {
subtitle: e.address,
},
};
}) ?? [];
</script>
<template>
<v-autocomplete
:label="label ?? 'Client'"
:model-value="modelValue"
:items="clients"
:clearable="optional"
@update:model-value="v => emit('update:modelValue', v)"
/>
</template>

View file

@ -1,10 +1,13 @@
<script setup>
import { useDisplay } from "vuetify/lib/framework.mjs";
import { ref } from "vue";
import { navigateTo } from "#app";
import { navigateTo, useRoute } from "#app";
const route = useRoute();
const { mobile } = useDisplay();
const navOpen = ref(!mobile.value);
const navSelected = ref([route.path]);
</script>
<template>
@ -13,9 +16,10 @@ const navOpen = ref(!mobile.value);
<VToolbarTitle>Database Project</VToolbarTitle>
</VAppBar>
<VNavigationDrawer v-model="navOpen" :temporary="mobile">
<VList density="compact" nav>
<VList v-model:selected="navSelected" density="compact" nav>
<VListItem prepend-icon="mdi-login" title="Login" value="/login" @click="navigateTo('/login')" />
<VListItem prepend-icon="mdi-account" title="Clients" value="/clients" @click="navigateTo('/clients')" />
<VListItem prepend-icon="mdi-receipt-text" title="Orders" value="/orders" @click="navigateTo('/orders')" />
<VDivider />
</VList>
</VNavigationDrawer>

6756
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,15 +16,14 @@
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"eslint": "^8.39.0",
"nuxt": "3.8.1",
"prisma": "5.5.2",
"nuxt": "3.8.2",
"prisma": "5.7.0",
"sass": "^1.62.0",
"vite-plugin-vuetify": "^1.0.2",
"vite-plugin-vuetify": "^2.0.1",
"vuetify": "^3.1.15"
},
"dependencies": {
"@prisma/client": "5.5.2",
"@prisma/migrate": "^5.5.2",
"mysql2": "^3.2.3"
"@prisma/client": "5.7.0",
"@prisma/migrate": "5.7.0"
}
}

View file

@ -84,7 +84,7 @@ const formData = ref<any>({
function normalizeForm() {
for (const i in formData.value)
formData.value[i] = formData.value[i] === "" ? null : formData.value[i];
formData.value[i] = formData.value[i] === "" || formData.value[i] === undefined ? null : formData.value[i];
}
async function handleSubmit() {
@ -97,6 +97,7 @@ async function handleSubmit() {
body: formData.value,
},
);
clients.value.unshift(result);
} catch (e) {
console.error(e);
submitting.value = false;

186
pages/orders.vue Normal file
View file

@ -0,0 +1,186 @@
<script setup lang="ts">
/* global $fetch */
import { ref, type Ref } from "vue";
import { VBtn } from "vuetify/components";
import type { NuxtError } from "#app";
import Alerts, { type AlertData } from "~/components/alerts.vue";
import { type fieldDefinition } from "~/components/entryEditor.vue";
import { definePageMeta, useFetch, createError, useRoute, navigateTo } from "#imports";
definePageMeta({ middleware: ["auth"] });
const route = useRoute();
const alerts = ref<Array<AlertData>>([]);
const ordersRequest = await useFetch("/api/orders");
if (ordersRequest.error.value) throw createError(ordersRequest.error.value?.data ?? "");
const orders = ordersRequest.data as Ref<NonNullable<typeof ordersRequest.data.value>>;
const countRequest = await useFetch("/api/orders/count");
if (countRequest.error.value) throw createError(countRequest.error.value?.data ?? "");
const count = countRequest.data as Ref<NonNullable<typeof countRequest.data.value>>;
const createMode = ref<boolean>(route.query?.create === "1");
async function rowClicked(client: string, edit = false) {
await navigateTo({
path: `/order/${client}`,
query: { edit: edit ? 1 : undefined },
});
}
async function rowDelete(client: string) {
try {
await $fetch(`/api/orders/${client}` as "api/orders/:id", {
method: "DELETE",
});
orders.value = orders.value.filter(e => e.id !== client);
count.value.count--;
} catch (e) {
alerts.value.push({ text: (e as NuxtError).message, type: "error" });
console.log(e);
}
}
const loadingMore = ref<boolean>(false);
async function loadBefore() {
loadingMore.value = true;
try {
orders.value.push(...await $fetch("/api/orders", {
query: {
before: orders.value[orders.value.length - 1].id,
},
}));
} catch (e) {
alerts.value.push({ text: (e as NuxtError).message });
console.error(e);
}
loadingMore.value = false;
}
function editorFields(): fieldDefinition[] {
return [
{ key: "clientId", type: "client", label: "Client" },
{ key: "draft", type: "boolean", label: "Draft", value: true },
{ key: "imported_products", type: "none", label: "imported_produts - TBA" },
{ key: "work", type: "none", label: "work - TBA" },
];
}
const submitting = ref<boolean>(false);
// const updateForm = ref<VForm | null>(null);
const formButton = ref<VBtn | null>(null);
const formData = ref<any>({});
function normalizeForm() {
for (const i in formData.value)
formData.value[i] = formData.value[i] === "" ? null : formData.value[i];
formData.value.imported_products = [];
formData.value.work = [];
}
async function handleSubmit() {
submitting.value = true;
normalizeForm();
try {
const result = await $fetch(
"/api/orders", {
method: "POST",
body: formData.value,
},
);
} catch (e) {
console.error(e);
submitting.value = false;
return;
}
submitting.value = false;
createMode.value = false;
}
</script>
<template>
<Alerts :alerts="alerts" />
<VDialog
v-model="createMode"
:persistent="submitting"
:activator="formButton as unknown as (Element | null) ?? undefined"
width="auto"
>
<VCard width="400px" :loading="submitting">
<VCardTitle>
Create client
</VCardTitle>
<VForm
ref="updateForm"
:disabled="submitting"
class="px-4"
>
<EntryEditor
:fields="editorFields()"
@update-sub-model-value="(k, v) => { formData[k] = v; }"
/>
</VForm>
<VCardActions>
<VBtn
color="primary"
@click="handleSubmit"
>
Submit
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<VRow>
<VCol>
<VBreadcrumbs :items="['Orders']" />
<VSpacer />
<div class="text-h4">
There are {{ count?.count }} orders in the database.
</div>
<VBtn
ref="formButton"
>
Create
</VBtn>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VTable>
<thead>
<tr>
<th>Client</th>
<th>Draft</th>
<th />
</tr>
</thead>
<pagedTable
buttons
:records="orders"
:fields="['client', 'Draft']"
record-key="id"
@click="(id) => rowClicked(id, false)"
@click:edit="(id) => rowClicked(id, true)"
@click:delete="(id) => rowDelete(id)"
/>
</VTable>
</VCard>
<VCol>
<VBtn
v-if="orders.length < count.count"
color="primary"
:loading="loadingMore"
@click="loadBefore"
>
Load more
</VBtn>
</VCol>
</VCol>
</VRow>
</template>

View file

@ -1,8 +1,8 @@
import url from "node:url";
import path from "node:path";
import { defineEventHandler, setResponseStatus, readBody } from "h3";
// @ts-expect-error
import { DbPush } from "@prisma/migrate";
// eslint-disable-next-line import/default
import PrismaMigrate from "@prisma/migrate";
import { database } from "../utils/database";
import { isFirstRun } from "./firstRun.get";
@ -39,7 +39,7 @@ export default defineEventHandler(async (e) => {
);
}
await DbPush.new().parse(dbPushParam);
await (new PrismaMigrate.DbPush()).parse(dbPushParam);
await database.user.create({
data: {
id: new Snowflake().state,

View file

@ -0,0 +1,9 @@
import { defineEventHandler } from "h3";
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
return {
count: await database.order.count({}),
};
});

View file

@ -4,12 +4,12 @@ let
# Updating this package will force an update for nodePackages.prisma. The
# version of prisma-engines and nodePackages.prisma must be the same for them to
# function correctly.
prisma-version = "5.5.2";
prisma-version = "5.7.0";
prisma-src = pkgs.fetchFromGitHub {
owner = "prisma";
repo = "prisma-engines";
rev = prisma-version;
hash = "sha256-d24b+Jobt5+vH7SGYOnDIR9DOtM0Y2XSfHZGkr7EidA=";
hash = "sha256-gZEz0UtgNwumsZbweAyx3TOVHJshpBigc9pzWN7Gb/A=";
};
new-prisma-engines = pkgs.rustPlatform.buildRustPackage {
pname = "prisma-engines";
@ -30,6 +30,7 @@ let
cargoLock = {
lockFile = "${prisma-src}/Cargo.lock";
outputHashes = {
"cuid-1.3.2" = "sha256-ZihFrLerEIOdbJggaBbByRbC1sZRvF4M0LN2albB7vA=";
"barrel-0.6.6-alpha.0" = "sha256-USh0lQ1z+3Spgc69bRFySUzhuY79qprLlEExTmYWFN8=";
"graphql-parser-0.3.0" = "sha256-0ZAsj2mW6fCLhwTETucjbu4rPNzfbNiHu2wVTBlTNe4=";
"mysql_async-0.31.3" = "sha256-QIO9s0Upc0/1W7ux1RNJNGKqzO4gB4gMV3NoakAbxkQ=";