TaskManagementSection.vue 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  1. <template>
  2. <div class="task-management">
  3. <h2>Tehtävien Hallinta</h2>
  4. <div class="task-controls">
  5. <button @click="showAddTaskModal" class="btn btn-primary">
  6. + Lisää uusi tehtävä
  7. </button>
  8. </div>
  9. <div class="task-list">
  10. <div v-if="loading" class="loading">Ladataan tehtäviä...</div>
  11. <div v-else-if="tasks.length === 0" class="no-tasks">
  12. Ei tehtäviä tallennettu
  13. </div>
  14. <div v-else class="tasks-grid">
  15. <div v-for="task in tasks" :key="task.id" class="task-card" :class="getTaskStatusClass(task.status)">
  16. <div class="task-header">
  17. <h3>{{ task.title }}</h3>
  18. <span class="task-status" :class="getStatusClass(task.status)">
  19. {{ getStatusText(task.status) }}
  20. </span>
  21. </div>
  22. <div class="task-details">
  23. <p v-if="task.description">{{ task.description }}</p>
  24. <div class="task-meta">
  25. <span class="task-date">
  26. <i class="fas fa-calendar"></i>
  27. {{ formatDate(task.created_at) }}
  28. </span>
  29. <span class="task-priority" :class="getPriorityClass(task.priority)">
  30. <i class="fas fa-flag"></i>
  31. {{ getPriorityText(task.priority) }}
  32. </span>
  33. <span class="task-hours" v-if="task.total_hours">
  34. <i class="fas fa-clock"></i>
  35. {{ task.total_hours }}h
  36. </span>
  37. </div>
  38. </div>
  39. <div class="task-actions">
  40. <button @click="showWorkHours(task)" class="btn btn-small btn-info">
  41. <i class="fas fa-clock"></i>
  42. Työtunnit
  43. </button>
  44. <button @click="toggleTimer(task)" class="btn btn-small" :class="hasActiveTimer(task.id) ? 'btn-danger' : 'btn-success'">
  45. <i class="fas" :class="hasActiveTimer(task.id) ? 'fa-stop' : 'fa-stopwatch'"></i>
  46. {{ hasActiveTimer(task.id) ? 'Pysäytä' : 'Aloita ajastin' }}
  47. </button>
  48. <button @click="editTask(task)" class="btn btn-small">
  49. <i class="fas fa-edit"></i>
  50. Muokkaa
  51. </button>
  52. <button @click="deleteTask(task.id)" class="btn btn-small btn-danger">
  53. <i class="fas fa-trash"></i>
  54. Poista
  55. </button>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. <!-- Add Task Modal -->
  61. <div v-if="showTaskModal" class="modal">
  62. <div class="modal-content">
  63. <div class="modal-header">
  64. <h3>{{ isEditing ? 'Muokkaa tehtävä' : 'Lisää uusi tehtävä' }}</h3>
  65. <button @click="closeTaskModal" class="close-btn">&times;</button>
  66. </div>
  67. <div class="modal-body">
  68. <form @submit.prevent="saveTask">
  69. <div class="form-group">
  70. <label for="taskTitle">Tehtävän nimi</label>
  71. <input
  72. id="taskTitle"
  73. v-model="taskForm.title"
  74. type="text"
  75. required
  76. placeholder="Syötä tehtävän nimi"
  77. />
  78. </div>
  79. <div class="form-group">
  80. <label for="taskDescription">Kuvaus</label>
  81. <textarea
  82. id="taskDescription"
  83. v-model="taskForm.description"
  84. rows="4"
  85. placeholder="Kuvaile tehtävä tarkemmin"
  86. ></textarea>
  87. </div>
  88. <div class="form-group">
  89. <label for="taskStatus">Tila</label>
  90. <select id="taskStatus" v-model="taskForm.status" required>
  91. <option value="pending">Odottaa</option>
  92. <option value="in_progress">Käynnissä</option>
  93. <option value="completed">Valmis</option>
  94. <option value="on_hold">Pidossa</option>
  95. <option value="cancelled">Peruttu</option>
  96. </select>
  97. </div>
  98. <div class="form-group">
  99. <label for="taskPriority">Prioriteetti</label>
  100. <select id="taskPriority" v-model="taskForm.priority" required>
  101. <option value="low">Matala</option>
  102. <option value="medium">Keskitaso</option>
  103. <option value="high">Korkea</option>
  104. </select>
  105. </div>
  106. <div class="form-group">
  107. <label for="taskProject">Projekti</label>
  108. <select id="taskProject" v-model="taskForm.project_id">
  109. <option value="">Valitse projekti</option>
  110. <option v-for="project in projects" :key="project.id" :value="project.id">
  111. {{ project.project_name }}
  112. </option>
  113. </select>
  114. </div>
  115. <div class="form-group">
  116. <label for="taskDueDate">Määräpäivä</label>
  117. <input
  118. id="taskDueDate"
  119. v-model="taskForm.due_date"
  120. type="date"
  121. />
  122. </div>
  123. </form>
  124. </div>
  125. <div class="modal-footer">
  126. <button type="submit" @click="saveTask" class="btn btn-primary">
  127. {{ isEditing ? 'Tallenna muutokset' : 'Lisää tehtävä' }}
  128. </button>
  129. <button @click="closeTaskModal" class="btn btn-secondary">
  130. Peruuta
  131. </button>
  132. </div>
  133. </div>
  134. </div>
  135. <!-- Work Hours Modal -->
  136. <div v-if="showWorkHoursModal" class="modal">
  137. <div class="modal-content work-hours-modal">
  138. <div class="modal-header">
  139. <h3>Työtunnit: {{ selectedTask?.title }}</h3>
  140. <button @click="closeWorkHoursModal" class="close-btn">&times;</button>
  141. </div>
  142. <div class="modal-body">
  143. <div class="work-hours-summary">
  144. <div class="summary-item">
  145. <span class="label">Kokonaistunnit:</span>
  146. <span class="value">{{ workHoursSummary.total_hours }}h</span>
  147. </div>
  148. <div class="summary-item">
  149. <span class="label">Tapahtumat:</span>
  150. <span class="value">{{ workHoursSummary.entries }}</span>
  151. </div>
  152. </div>
  153. <div class="work-hours-list">
  154. <div v-if="workHours.length === 0" class="no-work-hours">
  155. Ei työtunteja tallennettu
  156. </div>
  157. <div v-else>
  158. <div v-for="hour in workHours" :key="hour.id" class="work-hour-item">
  159. <div class="hour-info">
  160. <div class="hour-date">{{ formatDate(hour.date) }}</div>
  161. <div class="hour-user">{{ hour.first_name }} {{ hour.last_name }}</div>
  162. <div class="hour-hours">{{ hour.hours }}h</div>
  163. </div>
  164. <div class="hour-description" v-if="hour.description">
  165. {{ hour.description }}
  166. </div>
  167. <div class="hour-actions">
  168. <button @click="editWorkHour(hour)" class="btn btn-small">
  169. <i class="fas fa-edit"></i>
  170. Muokkaa
  171. </button>
  172. <button @click="deleteWorkHour(hour.id)" class="btn btn-small btn-danger">
  173. <i class="fas fa-trash"></i>
  174. Poista
  175. </button>
  176. </div>
  177. </div>
  178. </div>
  179. </div>
  180. <div class="add-work-hour">
  181. <button @click="showAddWorkHourModal" class="btn btn-primary">
  182. <i class="fas fa-plus"></i>
  183. Lisää työtunti
  184. </button>
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. <!-- Add/Edit Work Hour Modal -->
  190. <div v-if="showWorkHourModal" class="modal">
  191. <div class="modal-content">
  192. <div class="modal-header">
  193. <h3>{{ isEditingWorkHour ? 'Muokkaa työtuntia' : 'Lisää työtunti' }}</h3>
  194. <button @click="closeWorkHourModal" class="close-btn">&times;</button>
  195. </div>
  196. <div class="modal-body">
  197. <form @submit.prevent="saveWorkHour">
  198. <div class="form-group">
  199. <label for="workHourDate">Päivämäärä</label>
  200. <input
  201. id="workHourDate"
  202. v-model="workHourForm.date"
  203. type="date"
  204. required
  205. />
  206. </div>
  207. <div class="form-group">
  208. <label for="workHourHours">Tunnit</label>
  209. <input
  210. id="workHourHours"
  211. v-model="workHourForm.hours"
  212. type="number"
  213. step="0.25"
  214. min="0.25"
  215. max="24"
  216. required
  217. placeholder="Syötä tunnit"
  218. />
  219. </div>
  220. <div class="form-group">
  221. <label for="workHourRate">Tuntihinta (€)</label>
  222. <div class="rate-input-group">
  223. <input
  224. id="workHourRate"
  225. v-model="workHourForm.rate"
  226. type="number"
  227. step="0.01"
  228. min="0"
  229. placeholder="Syötä tuntihinta"
  230. />
  231. <button
  232. v-if="selectedTask?.client_hour_price"
  233. @click="useClientHourPrice"
  234. class="btn btn-small btn-info"
  235. type="button"
  236. >
  237. Käytä asiakkaan hintaa (€{{ selectedTask.client_hour_price }})
  238. </button>
  239. </div>
  240. </div>
  241. <div class="form-group">
  242. <label for="workHourDescription">Kuvaus</label>
  243. <textarea
  244. id="workHourDescription"
  245. v-model="workHourForm.description"
  246. rows="3"
  247. placeholder="Kuvaile työtä"
  248. ></textarea>
  249. </div>
  250. </form>
  251. </div>
  252. <div class="modal-footer">
  253. <button type="submit" @click="saveWorkHour" class="btn btn-primary">
  254. {{ isEditingWorkHour ? 'Tallenna muutokset' : 'Lisää työtunti' }}
  255. </button>
  256. <button @click="closeWorkHourModal" class="btn btn-secondary">
  257. Peruuta
  258. </button>
  259. </div>
  260. </div>
  261. </div>
  262. <!-- Timer Modal -->
  263. <div v-if="showTimerModal" class="modal">
  264. <div class="modal-content">
  265. <div class="modal-header">
  266. <h3>Ajastin: {{ selectedTask?.title }}</h3>
  267. <button @click="closeTimerModal" class="close-btn">&times;</button>
  268. </div>
  269. <div class="modal-body">
  270. <div class="timer-display" v-if="activeTimers.length > 0">
  271. <div class="timer-info">
  272. <div class="timer-status" :class="{ 'timer-running': activeTimers[0]?.end_time === null }">
  273. <i class="fas fa-circle"></i>
  274. {{ activeTimers[0]?.end_time === null ? 'Ajastin käynnissä' : 'Ajastin pysäytetty' }}
  275. </div>
  276. <div class="timer-duration">
  277. {{ formatTimerDuration(activeTimers[0]) }}
  278. </div>
  279. </div>
  280. </div>
  281. <div class="timer-controls">
  282. <button @click="startTimer" class="btn btn-success">
  283. <i class="fas fa-play"></i>
  284. Aloita
  285. </button>
  286. <button @click="stopTimer(activeTimers[0]?.id)" class="btn btn-danger" v-if="activeTimers[0]?.end_time === null">
  287. <i class="fas fa-stop"></i>
  288. Pysäytä
  289. </button>
  290. </div>
  291. </div>
  292. </div>
  293. </div>
  294. </div>
  295. </template>
  296. <script>
  297. import api from '../../api/axios'
  298. export default {
  299. name: 'TaskManagementSection',
  300. data() {
  301. return {
  302. tasks: [],
  303. projects: [],
  304. loading: false,
  305. showTaskModal: false,
  306. isEditing: false,
  307. taskForm: {
  308. id: null,
  309. title: '',
  310. description: '',
  311. status: 'pending',
  312. priority: 'medium',
  313. project_id: '',
  314. due_date: ''
  315. },
  316. // Work hours properties
  317. showWorkHoursModal: false,
  318. showWorkHourModal: false,
  319. isEditingWorkHour: false,
  320. selectedTask: null,
  321. workHours: [],
  322. workHoursSummary: {
  323. total_hours: 0,
  324. entries: 0
  325. },
  326. workHourForm: {
  327. id: null,
  328. task_id: null,
  329. user_id: 1, // Default to current user (should be dynamic)
  330. date: '',
  331. hours: '',
  332. rate: '',
  333. description: ''
  334. },
  335. // Timer properties
  336. showTimerModal: false,
  337. timerForm: {
  338. id: null,
  339. task_id: null,
  340. user_id: 1,
  341. start_time: '',
  342. end_time: '',
  343. duration: '',
  344. description: ''
  345. },
  346. activeTimers: [],
  347. timerDisplayProperties: {
  348. timerRunning: false,
  349. timerDuration: ''
  350. },
  351. timerModalProperties: {
  352. timerRunning: false,
  353. timerDuration: ''
  354. }
  355. }
  356. },
  357. mounted() {
  358. this.loadTasks()
  359. this.loadProjects()
  360. this.loadActiveTimers()
  361. },
  362. methods: {
  363. async loadTasks() {
  364. this.loading = true
  365. try {
  366. const response = await api.get('/tasks.php')
  367. if (response.data.success) {
  368. this.tasks = response.data.data
  369. // Load work hours summary for each task
  370. await this.loadTasksWorkHours()
  371. } else {
  372. console.error('Error loading tasks:', response.data.message)
  373. }
  374. } catch (error) {
  375. console.error('Error loading tasks:', error)
  376. } finally {
  377. this.loading = false
  378. }
  379. },
  380. async loadTasksWorkHours() {
  381. for (const task of this.tasks) {
  382. try {
  383. const response = await api.get(`/work_hours.php?task_id=${task.id}`)
  384. if (response.data.success) {
  385. const workHours = response.data.data
  386. task.total_hours = workHours.reduce((sum, hour) => sum + parseFloat(hour.hours), 0)
  387. } else {
  388. task.total_hours = 0
  389. }
  390. } catch (error) {
  391. console.error('Error loading work hours for Task:', task.id, error)
  392. task.total_hours = 0
  393. }
  394. }
  395. },
  396. async loadActiveTimers() {
  397. try {
  398. const response = await api.get('/timers.php?action=active')
  399. if (response.data.success) {
  400. this.activeTimers = response.data.data
  401. } else {
  402. this.activeTimers = []
  403. }
  404. } catch (error) {
  405. console.error('Error loading active timers:', error)
  406. this.activeTimers = []
  407. }
  408. },
  409. async loadProjects() {
  410. try {
  411. const response = await api.get('/projects.php')
  412. this.projects = response.data.records || []
  413. } catch (error) {
  414. console.error('Error loading projects:', error)
  415. }
  416. },
  417. showAddTaskModal() {
  418. this.isEditing = false
  419. this.taskForm = {
  420. id: null,
  421. title: '',
  422. description: '',
  423. status: 'pending',
  424. priority: 'medium',
  425. project_id: '',
  426. due_date: ''
  427. }
  428. this.showTaskModal = true
  429. },
  430. editTask(task) {
  431. this.isEditing = true
  432. this.taskForm = { ...task }
  433. this.showTaskModal = true
  434. },
  435. closeTaskModal() {
  436. this.showTaskModal = false
  437. this.isEditing = false
  438. },
  439. async saveTask() {
  440. try {
  441. let response
  442. if (this.isEditing) {
  443. response = await api.put(`/tasks.php?id=${this.taskForm.id}`, this.taskForm)
  444. } else {
  445. response = await api.post('/tasks.php', this.taskForm)
  446. }
  447. if (response.data.success) {
  448. this.closeTaskModal()
  449. this.loadTasks()
  450. } else {
  451. console.error('Error saving task:', response.data.message)
  452. }
  453. } catch (error) {
  454. console.error('Error saving task:', error)
  455. }
  456. },
  457. async deleteTask(taskId) {
  458. if (confirm('Haluatko varmasti poistaa tämän tehtävän?')) {
  459. try {
  460. await api.delete(`/api/tasks.php?id=${taskId}`)
  461. this.loadTasks()
  462. } catch (error) {
  463. console.error('Error deleting task:', error)
  464. }
  465. }
  466. },
  467. getTaskStatusClass(status) {
  468. return {
  469. 'pending': 'status-pending',
  470. 'in_progress': 'status-progress',
  471. 'completed': 'status-completed',
  472. 'on_hold': 'status-hold',
  473. 'cancelled': 'status-cancelled'
  474. }[status] || ''
  475. },
  476. getStatusClass(status) {
  477. return this.getTaskStatusClass(status)
  478. },
  479. getStatusText(status) {
  480. const statusTexts = {
  481. 'pending': 'Odottaa',
  482. 'in_progress': 'Käynnissä',
  483. 'completed': 'Valmis',
  484. 'on_hold': 'Pidossa',
  485. 'cancelled': 'Peruttu'
  486. }
  487. return statusTexts[status] || status
  488. },
  489. getPriorityClass(priority) {
  490. return {
  491. 'low': 'priority-low',
  492. 'medium': 'priority-medium',
  493. 'high': 'priority-high'
  494. }[priority] || ''
  495. },
  496. getPriorityText(priority) {
  497. const priorityTexts = {
  498. 'low': 'Matala',
  499. 'medium': 'Keskitaso',
  500. 'high': 'Korkea'
  501. }
  502. return priorityTexts[priority] || priority
  503. },
  504. formatDate(dateString) {
  505. if (!dateString) return ''
  506. const date = new Date(dateString)
  507. return date.toLocaleDateString('fi-FI')
  508. },
  509. // Work hours methods
  510. showWorkHours(task) {
  511. this.selectedTask = task
  512. this.loadWorkHours(task.id)
  513. this.loadTaskClientInfo(task.id)
  514. this.showWorkHoursModal = true
  515. },
  516. closeWorkHoursModal() {
  517. this.showWorkHoursModal = false
  518. this.selectedTask = null
  519. this.workHours = []
  520. this.workHoursSummary = { total_hours: 0, entries: 0 }
  521. },
  522. async loadWorkHours(taskId) {
  523. try {
  524. const response = await api.get(`/work_hours.php?task_id=${taskId}`)
  525. if (response.data.success) {
  526. this.workHours = response.data.data
  527. this.workHoursSummary = {
  528. total_hours: this.workHours.reduce((sum, hour) => sum + parseFloat(hour.hours), 0),
  529. entries: this.workHours.length
  530. }
  531. }
  532. } catch (error) {
  533. console.error('Error loading work hours:', error)
  534. }
  535. },
  536. showAddWorkHourModal() {
  537. this.isEditingWorkHour = false
  538. this.workHourForm = {
  539. id: null,
  540. task_id: this.selectedTask.id,
  541. user_id: 1, // Default to current user
  542. date: new Date().toISOString().split('T')[0],
  543. hours: '',
  544. rate: '',
  545. description: ''
  546. }
  547. this.showWorkHourModal = true
  548. },
  549. showTimer(task) {
  550. this.selectedTask = task
  551. this.loadActiveTimers()
  552. this.showTimerModal = true
  553. },
  554. closeTimerModal() {
  555. this.showTimerModal = false
  556. this.selectedTask = null
  557. this.activeTimers = []
  558. },
  559. async startTimer() {
  560. try {
  561. const response = await api.post('/timers.php', {
  562. action: 'start',
  563. task_id: this.selectedTask.id,
  564. user_id: 1, // Default to current user
  565. description: `Timer started for ${this.selectedTask.title}`
  566. })
  567. if (response.data.success) {
  568. this.loadActiveTimers()
  569. this.closeTimerModal()
  570. } else {
  571. console.error('Error starting timer:', response.data.message)
  572. }
  573. } catch (error) {
  574. console.error('Error starting timer:', error)
  575. }
  576. },
  577. async stopTimer(timerId) {
  578. try {
  579. const response = await api.post('/timers.php', {
  580. action: 'stop',
  581. id: timerId
  582. })
  583. if (response.data.success) {
  584. this.loadActiveTimers()
  585. this.loadTasks() // Refresh tasks to update total hours
  586. } else {
  587. console.error('Error stopping timer:', response.data.message)
  588. }
  589. } catch (error) {
  590. console.error('Error stopping timer:', error)
  591. }
  592. },
  593. async toggleTimer(task) {
  594. if (this.hasActiveTimer(task.id)) {
  595. // Stop the timer
  596. const activeTimer = this.activeTimers.find(timer => timer.task_id === task.id)
  597. if (activeTimer) {
  598. await this.stopTimer(activeTimer.id)
  599. }
  600. } else {
  601. // Start the timer
  602. await this.startTimer(task)
  603. }
  604. },
  605. hasActiveTimer(taskId) {
  606. return this.activeTimers.some(timer =>
  607. timer.task_id === parseInt(taskId) && timer.end_time === null
  608. )
  609. },
  610. formatTimerDuration(timer) {
  611. if (!timer.start_time) return '00:00:00'
  612. const start = new Date(timer.start_time)
  613. const now = new Date()
  614. const diff = now - start
  615. const hours = Math.floor(diff / (1000 * 60 * 60))
  616. const minutes = Math.floor((diff % (1000 * 60 * 60)) / 60000)
  617. const seconds = Math.floor((diff % 60000) / 1000)
  618. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  619. },
  620. editWorkHour(workHour) {
  621. this.isEditingWorkHour = true
  622. this.workHourForm = { ...workHour }
  623. this.showWorkHourModal = true
  624. },
  625. closeWorkHourModal() {
  626. this.showWorkHourModal = false
  627. this.isEditingWorkHour = false
  628. this.workHourForm = {
  629. id: null,
  630. task_id: null,
  631. user_id: 1,
  632. date: '',
  633. hours: '',
  634. rate: '',
  635. description: ''
  636. }
  637. },
  638. async saveWorkHour() {
  639. try {
  640. let response
  641. if (this.isEditingWorkHour) {
  642. response = await api.put(`/work_hours.php`, {
  643. ...this.workHourForm,
  644. id: this.workHourForm.id
  645. })
  646. } else {
  647. response = await api.post('/work_hours.php', this.workHourForm)
  648. }
  649. if (response.data.success) {
  650. this.closeWorkHourModal()
  651. this.loadWorkHours(this.selectedTask.id)
  652. this.loadTasks() // Refresh tasks to update total hours
  653. }
  654. } catch (error) {
  655. console.error('Error saving work hour:', error)
  656. }
  657. },
  658. async deleteWorkHour(workHourId) {
  659. if (confirm('Haluatko varmasti poistaa tämän työtunnin?')) {
  660. try {
  661. const response = await api.delete(`/work_hours.php?id=${workHourId}`)
  662. if (response.data.success) {
  663. this.loadWorkHours(this.selectedTask.id)
  664. this.loadTasks() // Refresh tasks to update total hours
  665. }
  666. } catch (error) {
  667. console.error('Error deleting work hour:', error)
  668. }
  669. }
  670. },
  671. async loadTaskClientInfo(taskId) {
  672. try {
  673. const response = await api.get(`/work_hours.php?task_id=${taskId}`)
  674. if (response.data.success && response.data.data.length > 0) {
  675. const firstWorkHour = response.data.data[0]
  676. this.selectedTask.client_hour_price = firstWorkHour.client_hour_price
  677. this.selectedTask.client_name = firstWorkHour.client_name ||
  678. `${firstWorkHour.client_first_name} ${firstWorkHour.client_last_name}`
  679. }
  680. } catch (error) {
  681. console.error('Error loading client info:', error)
  682. this.selectedTask.client_hour_price = null
  683. }
  684. },
  685. useClientHourPrice() {
  686. if (this.selectedTask?.client_hour_price) {
  687. this.workHourForm.rate = this.selectedTask.client_hour_price
  688. }
  689. }
  690. }
  691. }
  692. </script>
  693. <style scoped>
  694. .task-management {
  695. padding: 20px;
  696. }
  697. .task-controls {
  698. margin-bottom: 20px;
  699. }
  700. .task-list {
  701. min-height: 400px;
  702. }
  703. .loading {
  704. text-align: center;
  705. padding: 40px;
  706. color: #666;
  707. }
  708. .no-tasks {
  709. text-align: center;
  710. padding: 40px;
  711. color: #999;
  712. font-style: italic;
  713. }
  714. .tasks-grid {
  715. display: grid;
  716. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  717. gap: 20px;
  718. }
  719. .task-card {
  720. border: 1px solid #ddd;
  721. border-radius: 8px;
  722. padding: 20px;
  723. background: white;
  724. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  725. transition: transform 0.2s ease;
  726. }
  727. .task-card:hover {
  728. transform: translateY(-2px);
  729. box-shadow: 0 4px 8px rgba(0,0,0,0.15);
  730. }
  731. .task-header {
  732. display: flex;
  733. justify-content: space-between;
  734. align-items: center;
  735. margin-bottom: 10px;
  736. }
  737. .task-header h3 {
  738. margin: 0;
  739. color: #333;
  740. font-size: 1.2em;
  741. }
  742. .task-status {
  743. padding: 4px 8px;
  744. border-radius: 4px;
  745. font-size: 0.8em;
  746. font-weight: 500;
  747. }
  748. .status-pending {
  749. background: #fff3cd;
  750. color: #856404;
  751. }
  752. .status-progress {
  753. background: #cce5ff;
  754. color: #004085;
  755. }
  756. .status-completed {
  757. background: #d4edda;
  758. color: #155724;
  759. }
  760. .status-hold {
  761. background: #fff3cd;
  762. color: #856404;
  763. }
  764. .status-cancelled {
  765. background: #f8d7da;
  766. color: #721c24;
  767. }
  768. .task-details {
  769. margin-bottom: 15px;
  770. }
  771. .task-details p {
  772. margin: 0;
  773. color: #666;
  774. line-height: 1.4;
  775. }
  776. .task-meta {
  777. display: flex;
  778. gap: 15px;
  779. margin-bottom: 15px;
  780. }
  781. .task-date,
  782. .task-priority {
  783. display: flex;
  784. align-items: center;
  785. gap: 5px;
  786. font-size: 0.9em;
  787. }
  788. .task-date {
  789. color: #666;
  790. }
  791. .priority-low {
  792. background: #d4edda;
  793. color: #155724;
  794. }
  795. .priority-medium {
  796. background: #fff3cd;
  797. color: #856404;
  798. }
  799. .priority-high {
  800. background: #f8d7da;
  801. color: #721c24;
  802. }
  803. .task-actions {
  804. display: flex;
  805. gap: 10px;
  806. }
  807. .btn {
  808. padding: 8px 16px;
  809. border: none;
  810. border-radius: 4px;
  811. cursor: pointer;
  812. font-size: 0.9em;
  813. transition: background-color 0.2s ease;
  814. }
  815. .btn-small {
  816. padding: 6px 12px;
  817. font-size: 0.8em;
  818. }
  819. .btn-primary {
  820. background: #007bff;
  821. color: white;
  822. }
  823. .btn-primary:hover {
  824. background: #0056b3;
  825. }
  826. .btn-secondary {
  827. background: #6c757d;
  828. color: white;
  829. }
  830. .btn-danger {
  831. background: #dc3545;
  832. color: white;
  833. }
  834. .btn-danger:hover {
  835. background: #c82333;
  836. }
  837. /* Modal Styles */
  838. .modal {
  839. position: fixed;
  840. top: 0;
  841. left: 0;
  842. width: 100%;
  843. height: 100%;
  844. background: rgba(0,0,0,0.5);
  845. display: flex;
  846. align-items: center;
  847. justify-content: center;
  848. z-index: 1000;
  849. }
  850. .modal-content {
  851. background: white;
  852. border-radius: 8px;
  853. padding: 0;
  854. max-width: 600px;
  855. width: 90%;
  856. max-height: 80vh;
  857. overflow-y: auto;
  858. }
  859. .modal-header {
  860. display: flex;
  861. justify-content: space-between;
  862. align-items: center;
  863. padding: 20px;
  864. border-bottom: 1px solid #eee;
  865. }
  866. .modal-header h3 {
  867. margin: 0;
  868. color: #333;
  869. }
  870. .close-btn {
  871. background: none;
  872. border: none;
  873. font-size: 1.5em;
  874. cursor: pointer;
  875. color: #999;
  876. }
  877. .modal-body {
  878. padding: 20px;
  879. }
  880. .form-group {
  881. margin-bottom: 15px;
  882. }
  883. .form-group label {
  884. display: block;
  885. margin-bottom: 5px;
  886. font-weight: 500;
  887. color: #333;
  888. }
  889. .form-group input,
  890. .form-group textarea,
  891. .form-group select {
  892. width: 100%;
  893. padding: 10px;
  894. border: 1px solid #ddd;
  895. border-radius: 4px;
  896. font-size: 0.9em;
  897. }
  898. .form-group input:focus,
  899. .form-group textarea:focus,
  900. .form-group select:focus {
  901. outline: none;
  902. border-color: #007bff;
  903. box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
  904. }
  905. .modal-footer {
  906. display: flex;
  907. justify-content: flex-end;
  908. gap: 10px;
  909. padding: 20px;
  910. border-top: 1px solid #eee;
  911. }
  912. /* Work Hours Styles */
  913. .task-hours {
  914. background: #007bff;
  915. color: white;
  916. padding: 2px 6px;
  917. border-radius: 4px;
  918. font-size: 0.8em;
  919. }
  920. .btn-info {
  921. background: #17a2b8;
  922. color: white;
  923. }
  924. .btn-info:hover {
  925. background: #138496;
  926. }
  927. .work-hours-modal {
  928. max-width: 800px;
  929. }
  930. .work-hours-summary {
  931. display: flex;
  932. gap: 20px;
  933. margin-bottom: 20px;
  934. padding: 15px;
  935. background: #f8f9fa;
  936. border-radius: 8px;
  937. }
  938. .summary-item {
  939. display: flex;
  940. flex-direction: column;
  941. }
  942. .summary-item .label {
  943. font-size: 0.9em;
  944. color: #666;
  945. margin-bottom: 5px;
  946. }
  947. .summary-item .value {
  948. font-size: 1.2em;
  949. font-weight: bold;
  950. color: #333;
  951. }
  952. .work-hours-list {
  953. max-height: 300px;
  954. overflow-y: auto;
  955. margin-bottom: 20px;
  956. }
  957. .no-work-hours {
  958. text-align: center;
  959. padding: 40px;
  960. color: #999;
  961. font-style: italic;
  962. }
  963. .work-hour-item {
  964. border: 1px solid #eee;
  965. border-radius: 8px;
  966. padding: 15px;
  967. margin-bottom: 10px;
  968. background: white;
  969. }
  970. .hour-info {
  971. display: flex;
  972. justify-content: space-between;
  973. align-items: center;
  974. margin-bottom: 10px;
  975. }
  976. .hour-date {
  977. font-weight: bold;
  978. color: #333;
  979. }
  980. .hour-user {
  981. color: #666;
  982. }
  983. .hour-hours {
  984. background: #28a745;
  985. color: white;
  986. padding: 2px 8px;
  987. border-radius: 4px;
  988. font-weight: bold;
  989. }
  990. .hour-description {
  991. color: #666;
  992. margin-bottom: 10px;
  993. font-style: italic;
  994. }
  995. .hour-actions {
  996. display: flex;
  997. gap: 10px;
  998. }
  999. .add-work-hour {
  1000. text-align: center;
  1001. padding: 20px;
  1002. border-top: 1px solid #eee;
  1003. }
  1004. .rate-input-group {
  1005. display: flex;
  1006. gap: 10px;
  1007. align-items: center;
  1008. }
  1009. .rate-input-group input {
  1010. flex: 1;
  1011. }
  1012. .client-rate-info {
  1013. margin-top: 5px;
  1014. color: #666;
  1015. font-style: italic;
  1016. }
  1017. </style>