26/11/2019, 17:54

Tích hợp GanttChart vào project Laravel + VueJS

Gantt là sơ đồ ngang, dùng để trình bày các công việc và sự kiện theo thời gian. Sơ đồ sẽ gồm 2 phần chính: trục tung thể hiện tên các công việc và trục hoành thể hiện các mốc thời gian cho những công việc ấy. Nhìn vào một sơ đồ Gantt, bạn dễ dàng nắm bắt được các thông tin của từng đầu công việc ...

Gantt là sơ đồ ngang, dùng để trình bày các công việc và sự kiện theo thời gian. Sơ đồ sẽ gồm 2 phần chính: trục tung thể hiện tên các công việc và trục hoành thể hiện các mốc thời gian cho những công việc ấy. Nhìn vào một sơ đồ Gantt, bạn dễ dàng nắm bắt được các thông tin của từng đầu công việc và của cả dự án.

Chính vì cách bố trí thông tin đơn giản mà lại rõ ràng, trực quan nên nó đã trở thành công cụ hữu ích để lập kế hoạch, lên timeline thực hiện hoặc quản lý tiến độ dự án.

Một ví dụ đơn giản nhất về Gantt Chart là Redmine. Chúng ta có lẽ đã quá quen thuộc với redmine khi làm việc các dự án trên công ty. Trên redmine có support sẵn sơ đồ Gantt. Các bạn có thể thử mở dự án của mình ra để xem nhé. Dưới đây là một ví dụ về Gantt trên Redmine.

Để tích hợp Gantt Chart vào project ta có khá nhiều thư viện nhưng trong phạm vi bài viết này mình sẽ sử dụng thư viện dHtmlx Gantt. Đây là một thư viện mã nguồn mở với và đồng thời cũng có phiên bản trả phí gồm có các chức năng cao cấp hơn.

Một CSDL cơ bản nhất cho việc lưu trừ sơ đồ Gantt ta có các bảng sau:

  • Tasks: lưu trữ thông tin các đầu công việc, thời gian bắt đầu, thời gian kết thúc, tiến độ theo %...
  • Links: lưu trữ kết nối giữa các đầu việc

Ta tạo luôn migration trong project Laravel

php artisan make:model GanttTask -m
<?php
 
namespace App;
 
use IlluminateDatabaseEloquentModel;
 
class GanttTask extends Model
{
    protected $appends = ["open"];
 
    public function getOpenAttribute(){
        return true;
    }
}
<?php
 
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
 
class CreateGanttTasksTable extends Migration
{
    public function up()
    {
        Schema::create('gantt_tasks', function (Blueprint $table){
            $table->increments('id');
            $table->string('text');
            $table->integer('duration');
            $table->float('progress');
            $table->dateTime('start_date');
            $table->integer('parent');
            $table->timestamps();
        });
    }
 
    public function down()
    {
        Schema::dropIfExists('tasks');
    }
}
php artisan make:model GanttLink -m 
<?php
 
namespace App;
 
use IlluminateDatabaseEloquentModel;
 
class GanttLink extends Model
{
}
<?php
 
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
 
class CreateGanttLinksTable extends Migration
{
    public function up()
    {
        Schema::create('gantt_links', function (Blueprint $table) {
            $table->increments('id');
            $table->string('type');
            $table->integer('source');
            $table->integer('target');
            $table->timestamps();
        });
    }
 
    public function down()
    {
        Schema::dropIfExists('links');
    }
}

Tiếp theo, ta sẽ xây dựng API để lưu trữ dữ liệu mỗi khi các thao tác được gửi lên từ phía client. Ta thêm 2 controller tương ứng với 2 model đã tạo ở trên.

<?php
namespace AppHttpControllers;
 
use IlluminateHttpRequest;
use AppGanttTask;
 
class GanttTaskController extends Controller
{
    public function store(Request $request){
 
        $task = new GanttTask();
 
        $task->text = $request->text;
        $task->start_date = $request->start_date;
        $task->duration = $request->duration;
        $task->progress = $request->has("progress") ? $request->progress : 0;
        $task->parent = $request->parent;
 
        $task->save();
 
        return response()->json([
            "action"=> "inserted",
            "tid" => $task->id
        ]);
    }
 
    public function update($id, Request $request){
        $task = GanttTask::find($id);
 
        $task->text = $request->text;
        $task->start_date = $request->start_date;
        $task->duration = $request->duration;
        $task->progress = $request->has("progress") ? $request->progress : 0;
        $task->parent = $request->parent;
 
        $task->save();
 
        return response()->json([
            "action"=> "updated"
        ]);
    }
 
    public function destroy($id){
        $task = GanttTask::find($id);
        $task->delete();
 
        return response()->json([
            "action"=> "deleted"
        ]);
    }
}

Ở controller này ta có 3 hàm cơ bản để lưu trữ và cập nhật dữ liệu gồm: thêm, sửa, xóa. Thêm route tương ứng:

<?php
Route::resource('tasks', 'GanttTaskController');

Tiếp tục thêm controller cho Link:

<?php
namespace AppHttpControllers;
 
use IlluminateHttpRequest;
use AppGanttLink;
 
class GanttLinkController extends Controller
{
    public function store(Request $request){
        $link = new GanttLink();
 
        $link->type = $request->type;
        $link->source = $request->source;
        $link->target = $request->target;
 
        $link->save();
 
        return response()->json([
            "action"=> "inserted",
            "tid" => $link->id
        ]);
    }
 
    public function update($id, Request $request){
        $link = GanttLink::find($id);
 
        $link->type = $request->type;
        $link->source = $request->source;
        $link->target = $request->target;
 
        $link->save();
 
        return response()->json([
            "action"=> "updated"
        ]);
    }
 
    public function destroy($id){
        $link = GanttLink::find($id);
        $link->delete();
 
        return response()->json([
            "action"=> "deleted"
        ]);
    }
}

Routes:

Route::resource('links', 'GanttLinkController');

Project mình dùng demo lần này mình có sử dụng VueJS nên mình sẽ hướng đến việc viết Gantt thành component để tiện sử dụng lúc nào cần thiết. Đầu tiên để cài đặt dhtmlx ta cài đặt qua npm:

npm install dhtmlx-gantt --save

Tạo component Gantt:

<template>
  <div ref="gantt" style="height: 70vh;, awidth: 100%" />
</template>

<script>
import 'dhtmlx-gantt';

import 'dhtmlx-gantt/codebase/ext/dhtmlxgantt_marker';
import { fileDragAndDrop } from '@/snippets/dhx_file_dnd.js';

export default {
  name: 'Gantt',
  data() {
    return {
      dp: null,
      fileDnD: null,
    };
  },
  computed: {
    api_url() {
      return '/api/gantt;
    }
  },

  mounted () {
    //this.$_initGanttEvents();
    gantt.config.fit_tasks = true;
    gantt.config.grid_awidth = 500;
    gantt.config.columns = [
      { name: 'id', label: 'No', align:'center', awidth:35, template: function (task) {
        return task.$index + 1;
      }},
      { name:'text', label: 'Tên công việc', tree:true, awidth:'*'},
      { name:'start_date', label: 'Start', align:'center', awidth:80},
      { name:'duration', align:'center', awidth:70 },
      { name:'add', awidth:44 }
    ];

    gantt.locale.labels['section_progress'] = 'Progress';
    gantt.config.lightbox.sections = [
      {name: 'description', height: 38, map_to: 'text', type: 'textarea', focus: true},
      {
        name: 'progress', height: 22, map_to: 'progress', type: 'select', options: [
          {key: '0', label: 'Not started'},
          {key: '0.1', label: '10%'},
          {key: '0.2', label: '20%'},
          {key: '0.3', label: '30%'},
          {key: '0.4', label: '40%'},
          {key: '0.5', label: '50%'},
          {key: '0.6', label: '60%'},
          {key: '0.7', label: '70%'},
          {key: '0.8', label: '80%'},
          {key: '0.9', label: '90%'},
          {key: '1', label: 'Complete'}
        ]
      },
      {name: 'time', type: 'duration', map_to: 'auto', height: 50}
    ];

    gantt.config.date_grid = '%d/%m/%Y';
    gantt.config.date_format = '%Y-%m-%d %H:%i';

    gantt.config.scale_height = 50;
    gantt.config.scales = [
      {unit: 'month', step: 1, format: 'Tháng %m, %Y'},
      {unit: 'day', step: 1, format: '%j'}
    ];
  gantt.init(this.$refs.gantt);
  gantt.clearAll();
  gantt.ajax.get({
    url: this.api_url,
    headers: {
      'Authorization': 'Bearer ' + window.token
    }
  }).then(function (xhr) {
    gantt.parse(xhr.responseText);
  });
  this.dp = gantt.createDataProcessor({
    url: this.api_url,
    mode:'REST',
  });
  this.dp.setTransactionMode({
    headers: {
      'Authorization': 'Bearer ' + window.token,
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  }, true);

  this.fileDnD = fileDragAndDrop();
  this.fileDnD.init(gantt.$container);

  this.initImportFromMSProject();
    

    var date_to_str = gantt.date.date_to_str(gantt.config.task_date);
    var markerId = gantt.addMarker({
      start_date: new Date(),
      css: 'today',
      text: 'Today',
      title:date_to_str( new Date())
    });

  },
  beforeDestroy() {
    if (this.dp) {
      this.dp.destructor();
    }
  },

  methods: {
    $_initGanttEvents: function () {
      gantt.attachEvent('onTaskSelected', (id) => {
        let task = gantt.getTask(id);
        this.$emit('task-selected', task);
      });

      gantt.attachEvent('onAfterTaskAdd', (id, task) => {
        this.$emit('task-updated', id, 'inserted', task);
        task.progress = task.progress || 0;
        if(gantt.getSelectedId() == id) {
          this.$emit('task-selected', task);
        }
      });

      gantt.attachEvent('onAfterTaskUpdate', (id, task) => {
        this.$emit('task-updated', id, 'updated', task);
      });

      gantt.attachEvent('onAfterTaskDelete', (id) => {
        this.$emit('task-updated', id, 'deleted');
        if(!gantt.getSelectedId()) {
          this.$emit('task-selected', null);
        }
      }
                                          
0