| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154 |
- <template>
- <div class="task-management">
- <h2>Tehtävien Hallinta</h2>
-
- <div class="task-controls">
- <button @click="showAddTaskModal" class="btn btn-primary">
- + Lisää uusi tehtävä
- </button>
- </div>
-
- <div class="task-list">
- <div v-if="loading" class="loading">Ladataan tehtäviä...</div>
-
- <div v-else-if="tasks.length === 0" class="no-tasks">
- Ei tehtäviä tallennettu
- </div>
-
- <div v-else class="tasks-grid">
- <div v-for="task in tasks" :key="task.id" class="task-card" :class="getTaskStatusClass(task.status)">
- <div class="task-header">
- <h3>{{ task.title }}</h3>
- <span class="task-status" :class="getStatusClass(task.status)">
- {{ getStatusText(task.status) }}
- </span>
- </div>
-
- <div class="task-details">
- <p v-if="task.description">{{ task.description }}</p>
-
- <div class="task-meta">
- <span class="task-date">
- <i class="fas fa-calendar"></i>
- {{ formatDate(task.created_at) }}
- </span>
-
- <span class="task-priority" :class="getPriorityClass(task.priority)">
- <i class="fas fa-flag"></i>
- {{ getPriorityText(task.priority) }}
- </span>
-
- <span class="task-hours" v-if="task.total_hours">
- <i class="fas fa-clock"></i>
- {{ task.total_hours }}h
- </span>
- </div>
- </div>
-
- <div class="task-actions">
- <button @click="showWorkHours(task)" class="btn btn-small btn-info">
- <i class="fas fa-clock"></i>
- Työtunnit
- </button>
- <button @click="toggleTimer(task)" class="btn btn-small" :class="hasActiveTimer(task.id) ? 'btn-danger' : 'btn-success'">
- <i class="fas" :class="hasActiveTimer(task.id) ? 'fa-stop' : 'fa-stopwatch'"></i>
- {{ hasActiveTimer(task.id) ? 'Pysäytä' : 'Aloita ajastin' }}
- </button>
- <button @click="editTask(task)" class="btn btn-small">
- <i class="fas fa-edit"></i>
- Muokkaa
- </button>
- <button @click="deleteTask(task.id)" class="btn btn-small btn-danger">
- <i class="fas fa-trash"></i>
- Poista
- </button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Add Task Modal -->
- <div v-if="showTaskModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h3>{{ isEditing ? 'Muokkaa tehtävä' : 'Lisää uusi tehtävä' }}</h3>
- <button @click="closeTaskModal" class="close-btn">×</button>
- </div>
-
- <div class="modal-body">
- <form @submit.prevent="saveTask">
- <div class="form-group">
- <label for="taskTitle">Tehtävän nimi</label>
- <input
- id="taskTitle"
- v-model="taskForm.title"
- type="text"
- required
- placeholder="Syötä tehtävän nimi"
- />
- </div>
-
- <div class="form-group">
- <label for="taskDescription">Kuvaus</label>
- <textarea
- id="taskDescription"
- v-model="taskForm.description"
- rows="4"
- placeholder="Kuvaile tehtävä tarkemmin"
- ></textarea>
- </div>
-
- <div class="form-group">
- <label for="taskStatus">Tila</label>
- <select id="taskStatus" v-model="taskForm.status" required>
- <option value="pending">Odottaa</option>
- <option value="in_progress">Käynnissä</option>
- <option value="completed">Valmis</option>
- <option value="on_hold">Pidossa</option>
- <option value="cancelled">Peruttu</option>
- </select>
- </div>
-
- <div class="form-group">
- <label for="taskPriority">Prioriteetti</label>
- <select id="taskPriority" v-model="taskForm.priority" required>
- <option value="low">Matala</option>
- <option value="medium">Keskitaso</option>
- <option value="high">Korkea</option>
- </select>
- </div>
-
- <div class="form-group">
- <label for="taskProject">Projekti</label>
- <select id="taskProject" v-model="taskForm.project_id">
- <option value="">Valitse projekti</option>
- <option v-for="project in projects" :key="project.id" :value="project.id">
- {{ project.project_name }}
- </option>
- </select>
- </div>
-
- <div class="form-group">
- <label for="taskDueDate">Määräpäivä</label>
- <input
- id="taskDueDate"
- v-model="taskForm.due_date"
- type="date"
- />
- </div>
- </form>
- </div>
-
- <div class="modal-footer">
- <button type="submit" @click="saveTask" class="btn btn-primary">
- {{ isEditing ? 'Tallenna muutokset' : 'Lisää tehtävä' }}
- </button>
- <button @click="closeTaskModal" class="btn btn-secondary">
- Peruuta
- </button>
- </div>
- </div>
- </div>
-
- <!-- Work Hours Modal -->
- <div v-if="showWorkHoursModal" class="modal">
- <div class="modal-content work-hours-modal">
- <div class="modal-header">
- <h3>Työtunnit: {{ selectedTask?.title }}</h3>
- <button @click="closeWorkHoursModal" class="close-btn">×</button>
- </div>
-
- <div class="modal-body">
- <div class="work-hours-summary">
- <div class="summary-item">
- <span class="label">Kokonaistunnit:</span>
- <span class="value">{{ workHoursSummary.total_hours }}h</span>
- </div>
- <div class="summary-item">
- <span class="label">Tapahtumat:</span>
- <span class="value">{{ workHoursSummary.entries }}</span>
- </div>
- </div>
-
- <div class="work-hours-list">
- <div v-if="workHours.length === 0" class="no-work-hours">
- Ei työtunteja tallennettu
- </div>
-
- <div v-else>
- <div v-for="hour in workHours" :key="hour.id" class="work-hour-item">
- <div class="hour-info">
- <div class="hour-date">{{ formatDate(hour.date) }}</div>
- <div class="hour-user">{{ hour.first_name }} {{ hour.last_name }}</div>
- <div class="hour-hours">{{ hour.hours }}h</div>
- </div>
- <div class="hour-description" v-if="hour.description">
- {{ hour.description }}
- </div>
- <div class="hour-actions">
- <button @click="editWorkHour(hour)" class="btn btn-small">
- <i class="fas fa-edit"></i>
- Muokkaa
- </button>
- <button @click="deleteWorkHour(hour.id)" class="btn btn-small btn-danger">
- <i class="fas fa-trash"></i>
- Poista
- </button>
- </div>
- </div>
- </div>
- </div>
-
- <div class="add-work-hour">
- <button @click="showAddWorkHourModal" class="btn btn-primary">
- <i class="fas fa-plus"></i>
- Lisää työtunti
- </button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Add/Edit Work Hour Modal -->
- <div v-if="showWorkHourModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h3>{{ isEditingWorkHour ? 'Muokkaa työtuntia' : 'Lisää työtunti' }}</h3>
- <button @click="closeWorkHourModal" class="close-btn">×</button>
- </div>
-
- <div class="modal-body">
- <form @submit.prevent="saveWorkHour">
- <div class="form-group">
- <label for="workHourDate">Päivämäärä</label>
- <input
- id="workHourDate"
- v-model="workHourForm.date"
- type="date"
- required
- />
- </div>
-
- <div class="form-group">
- <label for="workHourHours">Tunnit</label>
- <input
- id="workHourHours"
- v-model="workHourForm.hours"
- type="number"
- step="0.25"
- min="0.25"
- max="24"
- required
- placeholder="Syötä tunnit"
- />
- </div>
-
- <div class="form-group">
- <label for="workHourRate">Tuntihinta (€)</label>
- <div class="rate-input-group">
- <input
- id="workHourRate"
- v-model="workHourForm.rate"
- type="number"
- step="0.01"
- min="0"
- placeholder="Syötä tuntihinta"
- />
- <button
- v-if="selectedTask?.client_hour_price"
- @click="useClientHourPrice"
- class="btn btn-small btn-info"
- type="button"
- >
- Käytä asiakkaan hintaa (€{{ selectedTask.client_hour_price }})
- </button>
- </div>
- </div>
-
- <div class="form-group">
- <label for="workHourDescription">Kuvaus</label>
- <textarea
- id="workHourDescription"
- v-model="workHourForm.description"
- rows="3"
- placeholder="Kuvaile työtä"
- ></textarea>
- </div>
- </form>
- </div>
-
- <div class="modal-footer">
- <button type="submit" @click="saveWorkHour" class="btn btn-primary">
- {{ isEditingWorkHour ? 'Tallenna muutokset' : 'Lisää työtunti' }}
- </button>
- <button @click="closeWorkHourModal" class="btn btn-secondary">
- Peruuta
- </button>
- </div>
- </div>
- </div>
-
- <!-- Timer Modal -->
- <div v-if="showTimerModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h3>Ajastin: {{ selectedTask?.title }}</h3>
- <button @click="closeTimerModal" class="close-btn">×</button>
- </div>
-
- <div class="modal-body">
- <div class="timer-display" v-if="activeTimers.length > 0">
- <div class="timer-info">
- <div class="timer-status" :class="{ 'timer-running': activeTimers[0]?.end_time === null }">
- <i class="fas fa-circle"></i>
- {{ activeTimers[0]?.end_time === null ? 'Ajastin käynnissä' : 'Ajastin pysäytetty' }}
- </div>
- <div class="timer-duration">
- {{ formatTimerDuration(activeTimers[0]) }}
- </div>
- </div>
- </div>
-
- <div class="timer-controls">
- <button @click="startTimer" class="btn btn-success">
- <i class="fas fa-play"></i>
- Aloita
- </button>
- <button @click="stopTimer(activeTimers[0]?.id)" class="btn btn-danger" v-if="activeTimers[0]?.end_time === null">
- <i class="fas fa-stop"></i>
- Pysäytä
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import api from '../../api/axios'
- export default {
- name: 'TaskManagementSection',
- data() {
- return {
- tasks: [],
- projects: [],
- loading: false,
- showTaskModal: false,
- isEditing: false,
- taskForm: {
- id: null,
- title: '',
- description: '',
- status: 'pending',
- priority: 'medium',
- project_id: '',
- due_date: ''
- },
- // Work hours properties
- showWorkHoursModal: false,
- showWorkHourModal: false,
- isEditingWorkHour: false,
- selectedTask: null,
- workHours: [],
- workHoursSummary: {
- total_hours: 0,
- entries: 0
- },
- workHourForm: {
- id: null,
- task_id: null,
- user_id: 1, // Default to current user (should be dynamic)
- date: '',
- hours: '',
- rate: '',
- description: ''
- },
- // Timer properties
- showTimerModal: false,
- timerForm: {
- id: null,
- task_id: null,
- user_id: 1,
- start_time: '',
- end_time: '',
- duration: '',
- description: ''
- },
- activeTimers: [],
- timerDisplayProperties: {
- timerRunning: false,
- timerDuration: ''
- },
- timerModalProperties: {
- timerRunning: false,
- timerDuration: ''
- }
- }
- },
-
- mounted() {
- this.loadTasks()
- this.loadProjects()
- this.loadActiveTimers()
- },
-
- methods: {
- async loadTasks() {
- this.loading = true
- try {
- const response = await api.get('/tasks.php')
- if (response.data.success) {
- this.tasks = response.data.data
- // Load work hours summary for each task
- await this.loadTasksWorkHours()
- } else {
- console.error('Error loading tasks:', response.data.message)
- }
- } catch (error) {
- console.error('Error loading tasks:', error)
- } finally {
- this.loading = false
- }
- },
-
- async loadTasksWorkHours() {
- for (const task of this.tasks) {
- try {
- const response = await api.get(`/work_hours.php?task_id=${task.id}`)
- if (response.data.success) {
- const workHours = response.data.data
- task.total_hours = workHours.reduce((sum, hour) => sum + parseFloat(hour.hours), 0)
- } else {
- task.total_hours = 0
- }
- } catch (error) {
- console.error('Error loading work hours for Task:', task.id, error)
- task.total_hours = 0
- }
- }
- },
-
- async loadActiveTimers() {
- try {
- const response = await api.get('/timers.php?action=active')
- if (response.data.success) {
- this.activeTimers = response.data.data
- } else {
- this.activeTimers = []
- }
- } catch (error) {
- console.error('Error loading active timers:', error)
- this.activeTimers = []
- }
- },
-
- async loadProjects() {
- try {
- const response = await api.get('/projects.php')
- this.projects = response.data.records || []
- } catch (error) {
- console.error('Error loading projects:', error)
- }
- },
-
- showAddTaskModal() {
- this.isEditing = false
- this.taskForm = {
- id: null,
- title: '',
- description: '',
- status: 'pending',
- priority: 'medium',
- project_id: '',
- due_date: ''
- }
- this.showTaskModal = true
- },
-
- editTask(task) {
- this.isEditing = true
- this.taskForm = { ...task }
- this.showTaskModal = true
- },
-
- closeTaskModal() {
- this.showTaskModal = false
- this.isEditing = false
- },
-
- async saveTask() {
- try {
- let response
- if (this.isEditing) {
- response = await api.put(`/tasks.php?id=${this.taskForm.id}`, this.taskForm)
- } else {
- response = await api.post('/tasks.php', this.taskForm)
- }
-
- if (response.data.success) {
- this.closeTaskModal()
- this.loadTasks()
- } else {
- console.error('Error saving task:', response.data.message)
- }
- } catch (error) {
- console.error('Error saving task:', error)
- }
- },
-
- async deleteTask(taskId) {
- if (confirm('Haluatko varmasti poistaa tämän tehtävän?')) {
- try {
- await api.delete(`/api/tasks.php?id=${taskId}`)
- this.loadTasks()
- } catch (error) {
- console.error('Error deleting task:', error)
- }
- }
- },
-
- getTaskStatusClass(status) {
- return {
- 'pending': 'status-pending',
- 'in_progress': 'status-progress',
- 'completed': 'status-completed',
- 'on_hold': 'status-hold',
- 'cancelled': 'status-cancelled'
- }[status] || ''
- },
-
- getStatusClass(status) {
- return this.getTaskStatusClass(status)
- },
-
- getStatusText(status) {
- const statusTexts = {
- 'pending': 'Odottaa',
- 'in_progress': 'Käynnissä',
- 'completed': 'Valmis',
- 'on_hold': 'Pidossa',
- 'cancelled': 'Peruttu'
- }
- return statusTexts[status] || status
- },
-
- getPriorityClass(priority) {
- return {
- 'low': 'priority-low',
- 'medium': 'priority-medium',
- 'high': 'priority-high'
- }[priority] || ''
- },
-
- getPriorityText(priority) {
- const priorityTexts = {
- 'low': 'Matala',
- 'medium': 'Keskitaso',
- 'high': 'Korkea'
- }
- return priorityTexts[priority] || priority
- },
-
- formatDate(dateString) {
- if (!dateString) return ''
- const date = new Date(dateString)
- return date.toLocaleDateString('fi-FI')
- },
-
- // Work hours methods
- showWorkHours(task) {
- this.selectedTask = task
- this.loadWorkHours(task.id)
- this.loadTaskClientInfo(task.id)
- this.showWorkHoursModal = true
- },
-
- closeWorkHoursModal() {
- this.showWorkHoursModal = false
- this.selectedTask = null
- this.workHours = []
- this.workHoursSummary = { total_hours: 0, entries: 0 }
- },
-
- async loadWorkHours(taskId) {
- try {
- const response = await api.get(`/work_hours.php?task_id=${taskId}`)
- if (response.data.success) {
- this.workHours = response.data.data
- this.workHoursSummary = {
- total_hours: this.workHours.reduce((sum, hour) => sum + parseFloat(hour.hours), 0),
- entries: this.workHours.length
- }
- }
- } catch (error) {
- console.error('Error loading work hours:', error)
- }
- },
-
- showAddWorkHourModal() {
- this.isEditingWorkHour = false
- this.workHourForm = {
- id: null,
- task_id: this.selectedTask.id,
- user_id: 1, // Default to current user
- date: new Date().toISOString().split('T')[0],
- hours: '',
- rate: '',
- description: ''
- }
- this.showWorkHourModal = true
- },
-
- showTimer(task) {
- this.selectedTask = task
- this.loadActiveTimers()
- this.showTimerModal = true
- },
-
- closeTimerModal() {
- this.showTimerModal = false
- this.selectedTask = null
- this.activeTimers = []
- },
-
- async startTimer() {
- try {
- const response = await api.post('/timers.php', {
- action: 'start',
- task_id: this.selectedTask.id,
- user_id: 1, // Default to current user
- description: `Timer started for ${this.selectedTask.title}`
- })
-
- if (response.data.success) {
- this.loadActiveTimers()
- this.closeTimerModal()
- } else {
- console.error('Error starting timer:', response.data.message)
- }
- } catch (error) {
- console.error('Error starting timer:', error)
- }
- },
-
- async stopTimer(timerId) {
- try {
- const response = await api.post('/timers.php', {
- action: 'stop',
- id: timerId
- })
-
- if (response.data.success) {
- this.loadActiveTimers()
- this.loadTasks() // Refresh tasks to update total hours
- } else {
- console.error('Error stopping timer:', response.data.message)
- }
- } catch (error) {
- console.error('Error stopping timer:', error)
- }
- },
-
- async toggleTimer(task) {
- if (this.hasActiveTimer(task.id)) {
- // Stop the timer
- const activeTimer = this.activeTimers.find(timer => timer.task_id === task.id)
- if (activeTimer) {
- await this.stopTimer(activeTimer.id)
- }
- } else {
- // Start the timer
- await this.startTimer(task)
- }
- },
-
- hasActiveTimer(taskId) {
- return this.activeTimers.some(timer =>
- timer.task_id === parseInt(taskId) && timer.end_time === null
- )
- },
-
- formatTimerDuration(timer) {
- if (!timer.start_time) return '00:00:00'
-
- const start = new Date(timer.start_time)
- const now = new Date()
- const diff = now - start
-
- const hours = Math.floor(diff / (1000 * 60 * 60))
- const minutes = Math.floor((diff % (1000 * 60 * 60)) / 60000)
- const seconds = Math.floor((diff % 60000) / 1000)
-
- return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
- },
-
- editWorkHour(workHour) {
- this.isEditingWorkHour = true
- this.workHourForm = { ...workHour }
- this.showWorkHourModal = true
- },
-
- closeWorkHourModal() {
- this.showWorkHourModal = false
- this.isEditingWorkHour = false
- this.workHourForm = {
- id: null,
- task_id: null,
- user_id: 1,
- date: '',
- hours: '',
- rate: '',
- description: ''
- }
- },
-
- async saveWorkHour() {
- try {
- let response
- if (this.isEditingWorkHour) {
- response = await api.put(`/work_hours.php`, {
- ...this.workHourForm,
- id: this.workHourForm.id
- })
- } else {
- response = await api.post('/work_hours.php', this.workHourForm)
- }
-
- if (response.data.success) {
- this.closeWorkHourModal()
- this.loadWorkHours(this.selectedTask.id)
- this.loadTasks() // Refresh tasks to update total hours
- }
- } catch (error) {
- console.error('Error saving work hour:', error)
- }
- },
-
- async deleteWorkHour(workHourId) {
- if (confirm('Haluatko varmasti poistaa tämän työtunnin?')) {
- try {
- const response = await api.delete(`/work_hours.php?id=${workHourId}`)
- if (response.data.success) {
- this.loadWorkHours(this.selectedTask.id)
- this.loadTasks() // Refresh tasks to update total hours
- }
- } catch (error) {
- console.error('Error deleting work hour:', error)
- }
- }
- },
-
- async loadTaskClientInfo(taskId) {
- try {
- const response = await api.get(`/work_hours.php?task_id=${taskId}`)
- if (response.data.success && response.data.data.length > 0) {
- const firstWorkHour = response.data.data[0]
- this.selectedTask.client_hour_price = firstWorkHour.client_hour_price
- this.selectedTask.client_name = firstWorkHour.client_name ||
- `${firstWorkHour.client_first_name} ${firstWorkHour.client_last_name}`
- }
- } catch (error) {
- console.error('Error loading client info:', error)
- this.selectedTask.client_hour_price = null
- }
- },
-
- useClientHourPrice() {
- if (this.selectedTask?.client_hour_price) {
- this.workHourForm.rate = this.selectedTask.client_hour_price
- }
- }
- }
- }
- </script>
- <style scoped>
- .task-management {
- padding: 20px;
- }
- .task-controls {
- margin-bottom: 20px;
- }
- .task-list {
- min-height: 400px;
- }
- .loading {
- text-align: center;
- padding: 40px;
- color: #666;
- }
- .no-tasks {
- text-align: center;
- padding: 40px;
- color: #999;
- font-style: italic;
- }
- .tasks-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 20px;
- }
- .task-card {
- border: 1px solid #ddd;
- border-radius: 8px;
- padding: 20px;
- background: white;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- transition: transform 0.2s ease;
- }
- .task-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgba(0,0,0,0.15);
- }
- .task-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }
- .task-header h3 {
- margin: 0;
- color: #333;
- font-size: 1.2em;
- }
- .task-status {
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 0.8em;
- font-weight: 500;
- }
- .status-pending {
- background: #fff3cd;
- color: #856404;
- }
- .status-progress {
- background: #cce5ff;
- color: #004085;
- }
- .status-completed {
- background: #d4edda;
- color: #155724;
- }
- .status-hold {
- background: #fff3cd;
- color: #856404;
- }
- .status-cancelled {
- background: #f8d7da;
- color: #721c24;
- }
- .task-details {
- margin-bottom: 15px;
- }
- .task-details p {
- margin: 0;
- color: #666;
- line-height: 1.4;
- }
- .task-meta {
- display: flex;
- gap: 15px;
- margin-bottom: 15px;
- }
- .task-date,
- .task-priority {
- display: flex;
- align-items: center;
- gap: 5px;
- font-size: 0.9em;
- }
- .task-date {
- color: #666;
- }
- .priority-low {
- background: #d4edda;
- color: #155724;
- }
- .priority-medium {
- background: #fff3cd;
- color: #856404;
- }
- .priority-high {
- background: #f8d7da;
- color: #721c24;
- }
- .task-actions {
- display: flex;
- gap: 10px;
- }
- .btn {
- padding: 8px 16px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.9em;
- transition: background-color 0.2s ease;
- }
- .btn-small {
- padding: 6px 12px;
- font-size: 0.8em;
- }
- .btn-primary {
- background: #007bff;
- color: white;
- }
- .btn-primary:hover {
- background: #0056b3;
- }
- .btn-secondary {
- background: #6c757d;
- color: white;
- }
- .btn-danger {
- background: #dc3545;
- color: white;
- }
- .btn-danger:hover {
- background: #c82333;
- }
- /* Modal Styles */
- .modal {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0,0,0,0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- .modal-content {
- background: white;
- border-radius: 8px;
- padding: 0;
- max-width: 600px;
- width: 90%;
- max-height: 80vh;
- overflow-y: auto;
- }
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20px;
- border-bottom: 1px solid #eee;
- }
- .modal-header h3 {
- margin: 0;
- color: #333;
- }
- .close-btn {
- background: none;
- border: none;
- font-size: 1.5em;
- cursor: pointer;
- color: #999;
- }
- .modal-body {
- padding: 20px;
- }
- .form-group {
- margin-bottom: 15px;
- }
- .form-group label {
- display: block;
- margin-bottom: 5px;
- font-weight: 500;
- color: #333;
- }
- .form-group input,
- .form-group textarea,
- .form-group select {
- width: 100%;
- padding: 10px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 0.9em;
- }
- .form-group input:focus,
- .form-group textarea:focus,
- .form-group select:focus {
- outline: none;
- border-color: #007bff;
- box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
- }
- .modal-footer {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- padding: 20px;
- border-top: 1px solid #eee;
- }
- /* Work Hours Styles */
- .task-hours {
- background: #007bff;
- color: white;
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.8em;
- }
- .btn-info {
- background: #17a2b8;
- color: white;
- }
- .btn-info:hover {
- background: #138496;
- }
- .work-hours-modal {
- max-width: 800px;
- }
- .work-hours-summary {
- display: flex;
- gap: 20px;
- margin-bottom: 20px;
- padding: 15px;
- background: #f8f9fa;
- border-radius: 8px;
- }
- .summary-item {
- display: flex;
- flex-direction: column;
- }
- .summary-item .label {
- font-size: 0.9em;
- color: #666;
- margin-bottom: 5px;
- }
- .summary-item .value {
- font-size: 1.2em;
- font-weight: bold;
- color: #333;
- }
- .work-hours-list {
- max-height: 300px;
- overflow-y: auto;
- margin-bottom: 20px;
- }
- .no-work-hours {
- text-align: center;
- padding: 40px;
- color: #999;
- font-style: italic;
- }
- .work-hour-item {
- border: 1px solid #eee;
- border-radius: 8px;
- padding: 15px;
- margin-bottom: 10px;
- background: white;
- }
- .hour-info {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }
- .hour-date {
- font-weight: bold;
- color: #333;
- }
- .hour-user {
- color: #666;
- }
- .hour-hours {
- background: #28a745;
- color: white;
- padding: 2px 8px;
- border-radius: 4px;
- font-weight: bold;
- }
- .hour-description {
- color: #666;
- margin-bottom: 10px;
- font-style: italic;
- }
- .hour-actions {
- display: flex;
- gap: 10px;
- }
- .add-work-hour {
- text-align: center;
- padding: 20px;
- border-top: 1px solid #eee;
- }
- .rate-input-group {
- display: flex;
- gap: 10px;
- align-items: center;
- }
- .rate-input-group input {
- flex: 1;
- }
- .client-rate-info {
- margin-top: 5px;
- color: #666;
- font-style: italic;
- }
- </style>
|