feat(tasking): add projects feature and restructure backend
- Restructure backend code into Tasking module with organized endpoints - Add Project and Sprint entities with database migrations - Implement CRUD endpoints for projects (create, get, rename, delete) - Refactor task endpoints into Tasking namespace - Add integration test suite with Testcontainers and Respawn - Refactor frontend to use Pinia stores with dedicated API clients - Add DueDatePicker and DueTimePicker components for task scheduling - Add environment configuration for API base URL - Add infrastructure setup scripts for Docker/Postgres
This commit is contained in:
6
Tasker.Ui/.env
Normal file
6
Tasker.Ui/.env
Normal file
@@ -0,0 +1,6 @@
|
||||
# Development environment variables for Vite
|
||||
# You can override these locally without committing secrets.
|
||||
# Vite only exposes variables prefixed with VITE_.
|
||||
|
||||
# Backend API base URL for development
|
||||
VITE_API_BASE_URL=http://localhost:5239/
|
||||
5
Tasker.Ui/.env.production
Normal file
5
Tasker.Ui/.env.production
Normal file
@@ -0,0 +1,5 @@
|
||||
# Production environment variables for Vite
|
||||
# Replace the URL below with your production API endpoint.
|
||||
# Vite only exposes variables prefixed with VITE_.
|
||||
|
||||
VITE_API_BASE_URL=https://api.example.com/
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to Vuetify 3</title>
|
||||
<title>Done By Daylight</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
3000
Tasker.Ui/package-lock.json
generated
3000
Tasker.Ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,8 @@
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"axios": "^1.6.7",
|
||||
"axios": "^1.7.7",
|
||||
"date-fns": "^3.6.0",
|
||||
"pinia": "^2.1.7",
|
||||
"uuidv7": "^1.0.1",
|
||||
"vue": "^3.4.29",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
value="users"
|
||||
></v-list-item>
|
||||
</v-list>
|
||||
|
||||
<project-section></project-section>
|
||||
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar app
|
||||
@@ -67,31 +70,14 @@
|
||||
</v-app>
|
||||
|
||||
</template>
|
||||
|
||||
m
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import ProjectSection from '@/components/ProjectSection.vue'
|
||||
|
||||
const drawer = ref(true)
|
||||
const rail = ref(false)
|
||||
const group = ref(null)
|
||||
const items = ref([
|
||||
{
|
||||
title: 'Foo',
|
||||
value: 'foo'
|
||||
},
|
||||
{
|
||||
title: 'Bar',
|
||||
value: 'bar'
|
||||
},
|
||||
{
|
||||
title: 'Fizz',
|
||||
value: 'fizz'
|
||||
},
|
||||
{
|
||||
title: 'Buzz',
|
||||
value: 'buzz'
|
||||
}
|
||||
])
|
||||
|
||||
watch(group, () => {
|
||||
drawer.value = false
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
name: string; // [MaxLength(255)]
|
||||
description?: string | null; // [MaxLength(2048)]
|
||||
dueDate?: string | null;
|
||||
}
|
||||
|
||||
export interface TaskApiClient {
|
||||
getTasks(): Promise<Task[]>;
|
||||
|
||||
addTask(task: Task): Promise<Task>;
|
||||
}
|
||||
|
||||
export class TaskApiClientImpl implements TaskApiClient {
|
||||
private readonly baseUrl: string
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
async getTasks(): Promise<Task[]> {
|
||||
const response = await fetch(`${this.baseUrl}/tasks`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch tasks')
|
||||
}
|
||||
const data = await response.json()
|
||||
return data as Task[]
|
||||
}
|
||||
|
||||
async addTask(task: Task): Promise<Task> {
|
||||
const response = await fetch(`${this.baseUrl}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(task)
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add task')
|
||||
}
|
||||
const data = await response.json()
|
||||
return data as Task
|
||||
}
|
||||
|
||||
async completeTask(id: string): Promise<Task> {
|
||||
const response = await fetch(`${this.baseUrl}/tasks/${id}/complete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'CompleteOn': new Date().toISOString()
|
||||
})
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add task')
|
||||
}
|
||||
const data = await response.json()
|
||||
return data as Task
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function useTasks() {
|
||||
const tasks = ref<Task[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const taskApiClient = new TaskApiClientImpl('https://localhost:7055')
|
||||
|
||||
const fetchTasks = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
tasks.value = await taskApiClient.getTasks()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addTask = async (task: Task) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newTask = await taskApiClient.addTask(task)
|
||||
tasks.value.push(newTask)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const completeTask = async (id: string) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await taskApiClient.completeTask(id)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchTasks)
|
||||
|
||||
return {
|
||||
tasks,
|
||||
isLoading,
|
||||
error,
|
||||
fetchTasks,
|
||||
addTask,
|
||||
completeTask
|
||||
}
|
||||
}
|
||||
160
Tasker.Ui/src/components/DueDatePicker.vue
Normal file
160
Tasker.Ui/src/components/DueDatePicker.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="due-date-picker">
|
||||
<v-card>
|
||||
<!-- Date Search Input -->
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="Type a due date"
|
||||
variant="underlined"
|
||||
></v-text-field>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<!-- Quick Date Selection -->
|
||||
<v-list>
|
||||
<v-list-item @click="selectQuickDate('today')">
|
||||
<v-icon left>mdi-calendar-today</v-icon>
|
||||
<v-list-item-title>Today</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedToday }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="selectQuickDate('tomorrow')">
|
||||
<v-icon left>mdi-calendar-arrow-right</v-icon>
|
||||
<v-list-item-title>Tomorrow</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedTomorrow }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="selectQuickDate('weekend')">
|
||||
<v-icon left>mdi-calendar-weekend</v-icon>
|
||||
<v-list-item-title>This weekend</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedWeekend }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="selectQuickDate('nextWeek')">
|
||||
<v-icon left>mdi-calendar-arrow-right-outline</v-icon>
|
||||
<v-list-item-title>Next week</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedNextWeek }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<!-- Date Picker -->
|
||||
<v-date-picker v-model="selectedDate"></v-date-picker>
|
||||
|
||||
<v-btn @click="openTimePicker">Time</v-btn>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
text="Cancel"
|
||||
color="secondary"
|
||||
@click="cancel"
|
||||
></v-btn>
|
||||
|
||||
<v-btn
|
||||
text="Set Time"
|
||||
color="primary"
|
||||
@click="save"
|
||||
></v-btn>
|
||||
|
||||
</v-card-actions>
|
||||
|
||||
</v-card>
|
||||
|
||||
<!-- DueTimePicker Component -->
|
||||
<due-time-picker
|
||||
v-model="isTimeDialogOpen"
|
||||
:time="selectedTime"
|
||||
@cancel=""
|
||||
@save="handleTimeSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, defineEmits} from 'vue'
|
||||
import {format, addDays, nextMonday} from 'date-fns'
|
||||
import DueTimePicker from './DueTimePicker.vue'
|
||||
import type { Time } from './Time';
|
||||
|
||||
// Reactive state
|
||||
const selectedDate = ref<Date | null>(null)
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
const selectedTime = ref<Time | null>(null)
|
||||
|
||||
interface DueDateChanged {
|
||||
selectedDate: Date,
|
||||
selectedTime: Time,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: DueDateChanged],
|
||||
update: [value: number]
|
||||
}>()
|
||||
|
||||
// Dialog state for time picker
|
||||
const isTimeDialogOpen = ref<boolean>(false)
|
||||
|
||||
// Methods to get quick dates
|
||||
const today = new Date()
|
||||
const tomorrow = addDays(today, 1)
|
||||
const weekend = addDays(today, 6 - today.getDay())
|
||||
const nextWeek = nextMonday(today)
|
||||
|
||||
// Computed formatted dates
|
||||
const formattedToday = computed(() => format(today, 'PPPP'))
|
||||
const formattedTomorrow = computed(() => format(tomorrow, 'PPPP'))
|
||||
const formattedWeekend = computed(() => format(weekend, 'PPPP'))
|
||||
const formattedNextWeek = computed(() => format(nextWeek, 'PPPP'))
|
||||
|
||||
// Select quick date options
|
||||
function selectQuickDate(option: 'today' | 'tomorrow' | 'weekend' | 'nextWeek') {
|
||||
switch (option) {
|
||||
case 'today':
|
||||
selectedDate.value = today
|
||||
break
|
||||
case 'tomorrow':
|
||||
selectedDate.value = tomorrow
|
||||
break
|
||||
case 'weekend':
|
||||
selectedDate.value = weekend
|
||||
break
|
||||
case 'nextWeek':
|
||||
selectedDate.value = nextWeek
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('update', 1)
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
}
|
||||
|
||||
// Open Time Picker Dialog
|
||||
function openTimePicker() {
|
||||
isTimeDialogOpen.value = true
|
||||
}
|
||||
|
||||
// Handle Save Event from DueTimePicker
|
||||
function handleTimeSave(time: Time) {
|
||||
console.log('Time:', time.time)
|
||||
console.log('Duration:', time.duration)
|
||||
console.log('Time Zone:', time.timeZone)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.due-date-picker {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
100
Tasker.Ui/src/components/DueTimePicker.vue
Normal file
100
Tasker.Ui/src/components/DueTimePicker.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<v-dialog v-model="isTimeDialogOpen" width="400px">
|
||||
<v-card>
|
||||
<v-card-title>Time Picker</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- Time Picker or Manual Input -->
|
||||
<v-autocomplete
|
||||
v-model="selectedTime"
|
||||
:items="timeOptions"
|
||||
label="Select or Enter Time"
|
||||
clearable
|
||||
@blur="validateTime"
|
||||
></v-autocomplete>
|
||||
|
||||
<!-- Duration Picker -->
|
||||
<v-select
|
||||
v-model="selectedDuration"
|
||||
:items="durations"
|
||||
label="Duration"
|
||||
></v-select>
|
||||
|
||||
<!-- Time Zone Picker -->
|
||||
<v-select
|
||||
v-model="selectedTimeZone"
|
||||
:items="timeZones"
|
||||
label="Time zone"
|
||||
></v-select>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="secondary" @click="close">Cancel</v-btn>
|
||||
<v-btn color="primary" @click="saveTimeSettings">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue';
|
||||
import {type Time} from './Time';
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: [],
|
||||
ok: [value: Time]
|
||||
}>()
|
||||
|
||||
// Reactive state
|
||||
const selectedTime = ref<string | null>(null);
|
||||
const selectedDuration = ref<string | null>(null);
|
||||
const selectedTimeZone = ref<string | null>('Floating time');
|
||||
|
||||
// Duration and Time Zones options
|
||||
const durations = ref(['No duration', '15 mins', '30 mins', '1 hour', '2 hours']);
|
||||
const timeZones = ref(['Floating time', 'Pacific Time', 'Mountain Time', 'Central Time', 'Eastern Time']);
|
||||
|
||||
// Time options every 15 minutes
|
||||
const timeOptions = ref(generateTimeOptions());
|
||||
|
||||
// Generate time options in 15-minute intervals
|
||||
function generateTimeOptions() {
|
||||
const times = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 15) {
|
||||
const time = new Date(0, 0, 0, hour, minute).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
times.push(time);
|
||||
}
|
||||
}
|
||||
return times;
|
||||
}
|
||||
|
||||
// Validate manually entered time
|
||||
function validateTime() {
|
||||
if (!selectedTime.value) return;
|
||||
|
||||
const isValidTime = timeOptions.value.includes(selectedTime.value);
|
||||
if (!isValidTime) {
|
||||
// If the time is not valid, you might want to reset it or show an error
|
||||
console.log('Invalid time entered');
|
||||
}
|
||||
}
|
||||
|
||||
// Close the dialog
|
||||
function close() {
|
||||
emit('cancel', false);
|
||||
}
|
||||
|
||||
// Save Time, Duration, and Time Zone Settings
|
||||
function saveTimeSettings() {
|
||||
emit('ok', {
|
||||
time: selectedTime.value,
|
||||
duration: selectedDuration.value,
|
||||
timeZone: selectedTimeZone.value,
|
||||
});
|
||||
close();
|
||||
}
|
||||
|
||||
</script>
|
||||
49
Tasker.Ui/src/components/ProjectCreate.vue
Normal file
49
Tasker.Ui/src/components/ProjectCreate.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showForm = ref(false)
|
||||
const name = ref('')
|
||||
const description = ref(null)
|
||||
|
||||
function createProject() {
|
||||
console.log(`Create new project: ${name.value} - ${description.value}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn v-if="!showForm" @click="showForm = true">
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
Add New Project
|
||||
</v-btn>
|
||||
|
||||
<v-form v-else>
|
||||
|
||||
<v-card>
|
||||
<v-text-field
|
||||
label="Name"
|
||||
v-model="name"
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
label="Description"
|
||||
v-model="description"
|
||||
></v-text-field>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="success" @click="createProject">
|
||||
Submit
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="error" @click="showForm = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
26
Tasker.Ui/src/components/ProjectList.vue
Normal file
26
Tasker.Ui/src/components/ProjectList.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useProjectStore } from '@/stores/projectStore'
|
||||
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item
|
||||
v-for="(item, index) in projectStore.projects"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
17
Tasker.Ui/src/components/ProjectSection.vue
Normal file
17
Tasker.Ui/src/components/ProjectSection.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import ProjectList from '@/components/ProjectList.vue'
|
||||
import ProjectCreate from '@/components/ProjectCreate.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Projects</h1>
|
||||
|
||||
<project-list></project-list>
|
||||
|
||||
<project-create></project-create>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,26 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { type Task, useTasks } from '@/api/taskApi'
|
||||
import { uuidv7 } from 'uuidv7'
|
||||
import DueDatePicker from '@/components/DueDatePicker.vue'
|
||||
import { useTaskStore } from '@/stores/taskStore'
|
||||
|
||||
const name = ref('')
|
||||
const description = ref(null)
|
||||
const dueDate = ref(null)
|
||||
const name = ref<string>('')
|
||||
const description = ref<string | null>(null)
|
||||
const dueDate = ref<string | null>(null)
|
||||
|
||||
const { addTask } = useTasks()
|
||||
const modalDueDatePicker = ref<boolean>(false)
|
||||
|
||||
const doAddTask = () => {
|
||||
const newTask: Task = {
|
||||
function changeDueDate(): void {
|
||||
modalDueDatePicker.value = !modalDueDatePicker.value
|
||||
}
|
||||
|
||||
function onDueDateChanged(value: string): void {
|
||||
dueDate.value = value
|
||||
}
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
function doAddTask(): Task {
|
||||
taskStore.addTask({
|
||||
id: uuidv7(),
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
dueDate: dueDate.value
|
||||
}
|
||||
addTask(newTask)
|
||||
} as Task)
|
||||
}
|
||||
|
||||
const cancelAddTask = () => {
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -38,19 +48,28 @@ const cancelAddTask = () => {
|
||||
</v-text-field>
|
||||
|
||||
<div class="flex flex-row space-x-2">
|
||||
|
||||
<span>due-date</span>
|
||||
|
||||
|
||||
<v-btn @click="changeDueDate()">
|
||||
<span>due-date</span>
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="modalDueDatePicker"
|
||||
width="400px">
|
||||
<due-date-picker :value="dueDate"
|
||||
@value-changed="onDueDateChanged"
|
||||
></due-date-picker>
|
||||
</v-dialog>
|
||||
|
||||
<span>priority</span>
|
||||
<span>tags</span>
|
||||
<span>reminders</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class=" bg-teal-400 p-2 flex flex-row space-x-2 justify-end">
|
||||
|
||||
<div class="bg-teal-400 p-2 flex flex-row space-x-2 justify-end">
|
||||
|
||||
<span class="justify-start">projects</span>
|
||||
|
||||
|
||||
<div class="bg-red space-x-2 justify-end">
|
||||
<v-btn @click="cancelAddTask()"
|
||||
variant="outlined">
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { useTaskStore } from '@/stores/taskStore'
|
||||
import type { Task } from '@/stores/task'
|
||||
|
||||
import { useTasks } from '@/api/taskApi'
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
const { completeTask, tasks, isLoading, error } = useTasks()
|
||||
|
||||
function itemCompleted(id: string) {
|
||||
console.log(`completing task: ${id}`)
|
||||
completeTask(id)
|
||||
function toggleTaskComplete(task: Task) {
|
||||
taskStore.completeTask(task) // Call the API to save the change
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list v-if="!isLoading && !error">
|
||||
<v-list-item v-for="task in tasks" :key="task.id">
|
||||
<div class="flex p-1 bg-amber">
|
||||
<v-form v-if="!taskStore.isLoading && !taskStore.error">
|
||||
<div class="flex p-0 m-0 bg-amber"
|
||||
v-for="task in taskStore.tasks" :key="task.id">
|
||||
|
||||
<div>
|
||||
<v-checkbox-btn @change="itemCompleted(task.id)"
|
||||
></v-checkbox-btn>
|
||||
</div>
|
||||
|
||||
<div class="flex-column">
|
||||
<div class="">
|
||||
<div class="font-sans">{{ task.name }}</div>
|
||||
<div class="font-serif font-normal">Description {{ task.description }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<v-checkbox-btn :model-value="task.completedOn !== null"
|
||||
@change="toggleTaskComplete(task)"
|
||||
></v-checkbox-btn>
|
||||
</div>
|
||||
|
||||
<div>{{ task.dueDate }}</div>
|
||||
<!-- <div>{{ task.project.Name }}</div>-->
|
||||
</div>
|
||||
|
||||
<div class="flex-column">
|
||||
<div class="">
|
||||
<div class="font-sans font-medium">{{ task.name }}</div>
|
||||
<div class="font-sans font-normal">{{ task.description }}</div>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div>{{ task.dueDate }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</v-form>
|
||||
|
||||
<v-alert v-else-if="error" type="error">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<v-progress-circular v-else indeterminate></v-progress-circular>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
5
Tasker.Ui/src/components/Time.ts
Normal file
5
Tasker.Ui/src/components/Time.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Time{
|
||||
time: string,
|
||||
timeZone: string,
|
||||
duration: string,
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import {ref, computed} from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
50
Tasker.Ui/src/stores/httpClient.ts
Normal file
50
Tasker.Ui/src/stores/httpClient.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// src/api/axiosClient.ts
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
// Resolve API base URL from Vite environment variables.
|
||||
// Configure via `.env` (development) and `.env.production` (production):
|
||||
// VITE_API_BASE_URL=https://localhost:7055/
|
||||
// If not set, it will fall back to current origin.
|
||||
console.log(`BASE URL: ${import.meta.env.VITE_API_BASE_URL}`);
|
||||
const envBaseUrl = (import.meta as any)?.env?.VITE_API_BASE_URL as string | undefined
|
||||
const resolvedBaseUrl = envBaseUrl && envBaseUrl.trim().length > 0
|
||||
? envBaseUrl
|
||||
: (typeof window !== 'undefined' ? window.location.origin : '')
|
||||
|
||||
console.log(`RESOLVED BASE URL: ${resolvedBaseUrl}`);
|
||||
// Create an instance of axios
|
||||
const httpClient = axios.create({
|
||||
baseURL: resolvedBaseUrl, // API base URL from configuration
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // Default headers
|
||||
},
|
||||
timeout: 5000, // Set a timeout (optional)
|
||||
})
|
||||
|
||||
// Optionally, add request/response interceptors
|
||||
httpClient.interceptors.request.use(
|
||||
(config) => {
|
||||
// You can modify the request here, e.g., attach auth tokens
|
||||
// config.headers.Authorization = `Bearer ${yourToken}`
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
httpClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
// Handle errors globally, e.g., log out on 401
|
||||
if (error.response?.status === 401) {
|
||||
console.error('Unauthorized, redirect to login')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default httpClient
|
||||
17
Tasker.Ui/src/stores/project.ts
Normal file
17
Tasker.Ui/src/stores/project.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string; // [MaxLength(255)]
|
||||
description?: string | null; // [MaxLength(2048)]
|
||||
dueDate?: string | null;
|
||||
completedOn?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateProjectRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ChangeProjectDescription {
|
||||
id: string;
|
||||
completedOn: Date;
|
||||
}
|
||||
77
Tasker.Ui/src/stores/projectStore.ts
Normal file
77
Tasker.Ui/src/stores/projectStore.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { projectsApiClient } from '@/stores/projectsApiClient'
|
||||
import type { Project } from '@/stores/project'
|
||||
|
||||
export const useProjectStore = defineStore(
|
||||
'projectStore',
|
||||
() => {
|
||||
const projects = ref<Project[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
projectsApiClient
|
||||
.getProjects()
|
||||
.then(t => projects.value = t)
|
||||
.catch(e => error.value = (e as Error).message)
|
||||
.finally(isLoading.value = false)
|
||||
|
||||
const fetchProjects = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
projects.value = await projectsApiClient.getProjects()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addProject = async (project: project) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newProject = await projectsApiClient.createProject(project)
|
||||
projects.value.push(newProject)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const completeProject = async (project: Project) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
if (project.completedOn) {
|
||||
await projectsApiClient.uncompleteProject(project.id)
|
||||
project.completedOn = null
|
||||
} else {
|
||||
project.completedOn = new Date().toISOString()
|
||||
await projectsApiClient.completeProject(
|
||||
{
|
||||
id: project.id,
|
||||
completedOn: project.completedOn
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projects,
|
||||
isLoading,
|
||||
error,
|
||||
fetchProjects,
|
||||
addProject,
|
||||
completeProject
|
||||
}
|
||||
}
|
||||
)
|
||||
44
Tasker.Ui/src/stores/projectsApiClient.ts
Normal file
44
Tasker.Ui/src/stores/projectsApiClient.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Project, CreateProjectRequest, CompleteProjectRequest } from '@/stores/project'
|
||||
import httpClient from '@/stores/httpClient'
|
||||
|
||||
export const projectsApiClient = {
|
||||
// Fetch all Projects
|
||||
async getProjects(): Promise<Project[]> {
|
||||
const response = await httpClient.get<Project[]>('/projects')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Fetch a single Project by ID
|
||||
async getProjectById(id: string): Promise<Project> {
|
||||
const response = await httpClient.get<Project>(`/projects/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Create a new Project
|
||||
async createProject(request: CreateProjectRequest): Promise<Project> {
|
||||
const response = await httpClient.post<Project>('/projects', request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Update an existing Project
|
||||
async completeProject(request: CompleteProjectRequest): Promise<Project> {
|
||||
const response = await httpClient.put<Project>(
|
||||
`/projects/complete`,
|
||||
request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async uncompleteProject(id: string): Promise<Project> {
|
||||
const response = await httpClient.put<Project>(
|
||||
`/projects/uncomplete`,
|
||||
{
|
||||
id: id
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Delete a Project
|
||||
async deleteProject(id: string): Promise<void> {
|
||||
await httpClient.delete(`/projects/${id}`)
|
||||
},
|
||||
}
|
||||
17
Tasker.Ui/src/stores/task.ts
Normal file
17
Tasker.Ui/src/stores/task.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
name: string; // [MaxLength(255)]
|
||||
description?: string | null; // [MaxLength(2048)]
|
||||
dueDate?: string | null;
|
||||
completedOn?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateTaskRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CompleteTaskRequest {
|
||||
id: string;
|
||||
completedOn: Date;
|
||||
}
|
||||
77
Tasker.Ui/src/stores/taskStore.ts
Normal file
77
Tasker.Ui/src/stores/taskStore.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { tasksApiClient } from '@/stores/tasksApiClient'
|
||||
import type { Task } from '@/stores/task'
|
||||
|
||||
export const useTaskStore = defineStore(
|
||||
'taskStore',
|
||||
() => {
|
||||
const tasks = ref<Task[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
tasksApiClient
|
||||
.getTasks()
|
||||
.then(t => tasks.value = t)
|
||||
.catch(e => error.value = (e as Error).message)
|
||||
.finally(isLoading.value = false)
|
||||
|
||||
const fetchTasks = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
tasks.value = await tasksApiClient.getTasks()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addTask = async (task: Task) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newTask = await tasksApiClient.createTask(task)
|
||||
tasks.value.push(newTask)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const completeTask = async (task: Task) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
if (task.completedOn) {
|
||||
await tasksApiClient.uncompleteTask(task.id)
|
||||
task.completedOn = null
|
||||
} else {
|
||||
task.completedOn = new Date().toISOString()
|
||||
await tasksApiClient.completeTask(
|
||||
{
|
||||
id: task.id,
|
||||
completedOn: task.completedOn
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
isLoading,
|
||||
error,
|
||||
fetchTasks,
|
||||
addTask,
|
||||
completeTask
|
||||
}
|
||||
}
|
||||
)
|
||||
46
Tasker.Ui/src/stores/tasksApiClient.ts
Normal file
46
Tasker.Ui/src/stores/tasksApiClient.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Task, CreateTaskRequest, CompleteTaskRequest } from '@/stores/task'
|
||||
import httpClient from '@/stores/httpClient'
|
||||
|
||||
export const tasksApiClient = {
|
||||
// Fetch all Tasks
|
||||
async getTasks(): Promise<Task[]> {
|
||||
const response = await httpClient.get<Task[]>('/tasks')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Fetch a single Task by ID
|
||||
async getTaskById(id: string): Promise<Task> {
|
||||
const response = await httpClient.get<Task>(`/tasks/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Create a new Task
|
||||
async createTask(request: CreateTaskRequest): Promise<Task> {
|
||||
const response = await httpClient.post<Task>(
|
||||
'/tasks',
|
||||
request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Update an existing Task
|
||||
async completeTask(request: CompleteTaskRequest): Promise<Task> {
|
||||
const response = await httpClient.put<Task>(
|
||||
`/tasks/complete`,
|
||||
request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async uncompleteTask(id: string): Promise<Task> {
|
||||
const response = await httpClient.put<Task>(
|
||||
`/tasks/uncomplete`,
|
||||
{
|
||||
id: id
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Delete a Task
|
||||
async deleteTask(id: string): Promise<void> {
|
||||
await httpClient.delete(`/tasks/${id}`)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import {useCounterStore} from "@/stores/counter";
|
||||
import TaskInput from "@/components/TaskInput.vue";
|
||||
import TaskInput from '@/components/TaskInput.vue'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
|
||||
const counterStore = useCounterStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<task-input></task-input>
|
||||
<task-list></task-list>
|
||||
|
||||
<v-card class="m-2 p-2">
|
||||
<task-input></task-input>
|
||||
</v-card>
|
||||
|
||||
<v-card class="m-2 p-2">
|
||||
<task-list></task-list>
|
||||
</v-card>
|
||||
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user