This commit is contained in:
IT 老营长 @云轩领航-创始人 2025-05-08 11:58:43 +08:00
parent 29a22c00a5
commit d9f550d4b6
12 changed files with 452 additions and 49 deletions

View File

@ -1,7 +1,8 @@
package xingmuguanli package xiangmuguanli
import ( import (
"shylinux.com/x/ice" "shylinux.com/x/ice"
"shylinux.com/x/icebergs/base/mdb"
kit "shylinux.com/x/toolkits" kit "shylinux.com/x/toolkits"
"shylinux.com/x/enterprise/src/guanlixitong" "shylinux.com/x/enterprise/src/guanlixitong"
@ -10,7 +11,9 @@ import (
type Table struct { type Table struct {
guanlixitong.Table guanlixitong.Table
list string `name:"list project_uid uid auto" role:"void"` list string `name:"list project_uid uid auto" role:"void"`
taskList string `name:"taskList" role:"worker"`
doneList string `name:"doneList" role:"worker"`
} }
func (s Table) Inputs(m *ice.Message, arg ...string) { func (s Table) Inputs(m *ice.Message, arg ...string) {
@ -30,11 +33,46 @@ func (s Table) RewriteAppend(m *ice.Message, arg ...string) *ice.Message {
value = UserProjectRole(kit.Int(value)).String() value = UserProjectRole(kit.Int(value)).String()
case model.PROJECT_TYPE: case model.PROJECT_TYPE:
value = ProjectType(kit.Int(value)).String() value = ProjectType(kit.Int(value)).String()
case model.PLAN_STATUS:
value = PlanStatus(kit.Int(value)).String()
case model.TASK_STATUS:
value = TaskStatus(kit.Int(value)).String()
case model.DONE_STATUS:
value = DoneStatus(kit.Int(value)).String()
} }
return value return value
}) })
return s.Table.RewriteAppend(m) return s.Table.RewriteAppend(m)
} }
func (s Table) SelectJoinPlan(m *ice.Message, arg ...string) {
s.SelectJoin(m, Plan{}, model.TITLE, model.STATUS)
}
func (s Table) OtherCreate(m *ice.Message, target ice.Any, arg ...string) {
m.Cmdy(target, s.Create, m.CommandKey()+"_uid", m.Option(model.UID), arg)
kit.If(!m.IsErr(), func() { m.ProcessField(target, []string{m.Option(model.PROJECT_UID), m.Result()}) })
}
func (s Table) OtherList(m *ice.Message, target ice.Any, arg ...string) *ice.Message {
m.Cmdy(target, s.Select, m.OptionSimple(model.PROJECT_UID), m.CommandKey()+"_uid", m.Option(model.UID)).Option("_command", ice.GetTypeKey(target))
s.SelectJoinUser(m)
return m
}
func (s Table) finishCheck(m *ice.Message, target ice.Any, name string, arg ...string) bool {
count := m.Cmd(target, s.Select, m.CommandKey()+"_uid = ? AND status != ? AND status != ? AND status != ?",
m.Option(model.UID), TaskRejected, TaskFinish, TaskCancel).Length()
if m.WarnNotValid(count > 0, kit.Format("还有 %v 个未完成的%s", count, name)) {
return true
}
return false
}
func (s Table) ChangeStatus(m *ice.Message, from, to int, arg ...string) {
s.Table.ChangeStatus(m, m.Option(model.PROJECT_UID), m.Option(model.UID), from, to, arg...)
}
func (s Table) addCount(m *ice.Message, target ice.Any) Table {
m.Cmd(target, s.AddCount, m.CommandKey()+"_count", kit.Select("-1", "1", m.ActionKey() == mdb.CREATE), m.Option(model.UID))
return s
}
func (s Table) planCount(m *ice.Message) Table { return s.addCount(m, Plan{}) }
func (s Table) taskCount(m *ice.Message) Table { return s.addCount(m, Task{}) }
type Tables struct{ Table } type Tables struct{ Table }

View File

@ -1,16 +1,77 @@
package xingmuguanli package xiangmuguanli
import "shylinux.com/x/ice" import (
"shylinux.com/x/ice"
kit "shylinux.com/x/toolkits"
type done struct { "shylinux.com/x/enterprise/src/xiangmuguanli/model"
Tables )
order string `data:"5"`
fields string `data:"title,content,user_uid"` type Done struct {
create string `name:"create title* content*" role:"leader"` Table
remove string `name:"remove" role:"leader"` order string `data:"3"`
fields string `data:"title,content,done_status,begin_time,end_time,process_time,finish_time,task_uid,plan_uid,user_uid"`
create string `name:"create task_uid* title* content* begin_time:select@date end_time:select@date" role:"worker"`
modify string `name:"modify title* content* begin_time:select@date end_time:select@date" role:"worker"`
remove string `name:"remove" role:"worker"`
process string `name:"process" role:"worker"`
finish string `name:"finish" role:"worker"`
} }
func (s done) List(m *ice.Message, arg ...string) { func (s Done) Create(m *ice.Message, arg ...string) {
s.ValueCreate(m, kit.ArgDef(arg, kit.Simple(model.BEGIN_TIME, m.Time(), model.END_TIME, m.Time("72h"))...)...)
s.SendMessage(m, "", "")
s.taskCount(m).DashboardUpdate(m)
}
func (s Done) Remove(m *ice.Message, arg ...string) {
s.ValueRemove(m, arg...)
s.taskCount(m).DashboardUpdate(m)
}
func (s Done) List(m *ice.Message, arg ...string) {
user_uid := m.Option(model.USER_UID)
s.ValueList(m, arg).Table(func(value ice.Maps) {
button := []ice.Any{}
defer func() { m.PushButton(button...) }()
switch DoneStatus(kit.Int(value[model.DONE_STATUS])) {
case DoneCreate:
if value[model.USER_UID] == user_uid {
button = append(button, s.Process)
}
case DoneProcess:
if value[model.USER_UID] == user_uid {
button = append(button, s.Finish)
}
case DoneFinish:
kit.If(m.FieldsIsDetail(), func() { s.DoneMessage(m) })
}
}).Display("")
s.SelectJoinPlan(m)
}
func (s Done) Process(m *ice.Message, arg ...string) {
s.changeStatus(m, DoneCreate, DoneProcess)
}
func (s Done) Finish(m *ice.Message, arg ...string) {
s.changeStatus(m, DoneProcess, DoneFinish)
} }
func init() { ice.TeamCtxCmd(done{}) } func init() { ice.TeamCtxCmd(Done{}) }
func (s Done) changeStatus(m *ice.Message, from, to DoneStatus) {
s.ChangeStatus(m, int(from), int(to), m.ActionKey()+"_time", m.Time())
}
type DoneStatus int
const (
DoneCreate DoneStatus = iota
DoneProcess
DoneFinish
)
var DoneStatusList = map[DoneStatus]string{
DoneCreate: "create",
DoneProcess: "process",
DoneFinish: "finish",
}
func (s DoneStatus) String() string { return DoneStatusList[s] }

View File

@ -9,6 +9,16 @@ const (
TYPE = "type" TYPE = "type"
TITLE = "title" TITLE = "title"
CONTENT = "content" CONTENT = "content"
BEGIN_TIME = "begin_time"
END_TIME = "end_time"
STATUS = "status"
TASK_COUNT = "task_count"
DONE_COUNT = "done_count"
CREATED_AT = "created_at"
PLAN_STATUS = "plan_status"
TASK_STATUS = "task_status"
DONE_STATUS = "done_status"
MARKET_UID = "market_uid"
USER_UID = "user_uid" USER_UID = "user_uid"
USER_PROJECT_ROLE = "user_project_role" USER_PROJECT_ROLE = "user_project_role"
PROJECT_UID = "project_uid" PROJECT_UID = "project_uid"
@ -26,16 +36,41 @@ type Project struct {
CompanyUID string `gorm:"type:char(32);index"` CompanyUID string `gorm:"type:char(32);index"`
} }
type Plan struct { type Plan struct {
db.ModelContent Common
ProjectUID string `gorm:"type:char(32);index"` TaskCount int `gorm:"default:0"`
} }
type Task struct { type Task struct {
db.ModelContent Common
ProjectUID string `gorm:"type:char(32);index"` PlanUID string `gorm:"type:char(32);index"`
DoneCount int `gorm:"default:0"`
}
type Done struct {
Common
PlanUID string `gorm:"type:char(32);index"`
TaskUID string `gorm:"type:char(32);index"`
} }
type Todo struct { type Todo struct {
db.ModelContent db.ModelContent
ProjectUID string `gorm:"type:char(32);index"` ProjectUID string `gorm:"type:char(32);index"`
} }
type Date struct {
db.ModelContent
ProjectUID string `gorm:"type:char(32);index"`
Link string `gorm:"type:varchar(255)"`
BeginTime db.Time
EndTime db.Time
}
func init() { db.CmdModels("", &UserProject{}, &Project{}, &Plan{}, &Task{}, &Todo{}) } func init() {
db.CmdModels("", &UserProject{}, &Project{}, &Plan{}, &Task{}, &Done{}, &Todo{}, &Date{})
}
type Common struct {
db.ModelContent
ProjectUID string `gorm:"type:char(32);index"`
Status uint8 `gorm:"default:0"`
BeginTime db.Time
EndTime db.Time
ProcessTime db.Time
FinishTime db.Time
}

View File

@ -1,15 +1,102 @@
package xingmuguanli package xiangmuguanli
import "shylinux.com/x/ice" import (
"shylinux.com/x/ice"
kit "shylinux.com/x/toolkits"
type plan struct { "shylinux.com/x/enterprise/src/xiangmuguanli/model"
)
type Plan struct {
Table Table
order string `data:"1"` order string `data:"1"`
fields string `data:"title,content,user_uid"` fields string `data:"title,content,plan_status,task_count,begin_time,end_time,process_time,finish_time,user_uid"`
create string `name:"create title* content*" role:"leader"` create string `name:"create title* content* begin_time:select@date end_time:select@date" role:"leader"`
remove string `name:"remove" role:"leader"` modify string `name:"modify title* content* begin_time:select@date end_time:select@date" role:"leader"`
remove string `name:"remove" role:"leader"`
process string `name:"process" role:"leader"`
taskCreate string `name:"taskCreate title* content* begin_time:select@date end_time:select@date" role:"worker"`
} }
func (s plan) List(m *ice.Message, arg ...string) { s.ValueList(m, arg).Display("") } func (s Plan) Create(m *ice.Message, arg ...string) {
s.ValueCreate(m, kit.ArgDef(arg, kit.Simple(model.BEGIN_TIME, m.Time(), model.END_TIME, m.Time("168h"))...)...)
s.SendMessage(m, "", "")
}
func (s Plan) List(m *ice.Message, arg ...string) {
isLeader, isWorker := s.IsLeader(m), s.IsWorker(m)
s.Orders(m, model.STATUS, model.TASK_COUNT, s.Desc(model.CREATED_AT))
s.ValueList(m, arg).Table(func(value ice.Maps) {
button := []ice.Any{}
switch PlanStatus(kit.Int(value[model.PLAN_STATUS])) {
case PlanCreate:
if isLeader {
button = append(button, s.Process, s.Modify, s.Remove)
}
case PlanProcess:
if isWorker {
button = append(button, s.TaskCreate)
}
if isLeader && kit.Int(value[model.TASK_COUNT]) > 0 {
button = append(button, s.Finish)
}
if isLeader && m.Option(model.MARKET_UID) == "" {
button = append(button, s.MarketInsert)
}
if isLeader && kit.Int(value[model.TASK_COUNT]) == 0 {
button = append(button, s.Remove)
}
case PlanFinish:
kit.If(m.FieldsIsDetail(), func() { s.DoneMessage(m) })
}
m.PushButton(button...)
}).Display("")
if !s.IsLeader(m) {
if m.Action(); m.Length() == 0 {
m.SetResult("请等待「管理员」创建项目计划")
}
}
m.Option("otherList", "taskList,doneList")
}
func (s Plan) Process(m *ice.Message, arg ...string) {
s.changeStatus(m, PlanCreate, PlanProcess)
}
func (s Plan) Finish(m *ice.Message, arg ...string) {
if s.finishCheck(m, Task{}, "任务") {
return
}
if s.finishCheck(m, Done{}, "提报") {
return
}
s.changeStatus(m, PlanProcess, PlanFinish)
}
func (s Plan) TaskCreate(m *ice.Message, arg ...string) {
s.OtherCreate(m, Task{}, arg...)
}
func (s Plan) TaskList(m *ice.Message, arg ...string) {
s.OtherList(m, Task{}).Display("task.js")
}
func (s Plan) DoneList(m *ice.Message, arg ...string) {
s.OtherList(m, Done{}).Display("done.js")
}
func init() { ice.TeamCtxCmd(plan{}) } func init() { ice.TeamCtxCmd(Plan{}) }
func (s Plan) changeStatus(m *ice.Message, from, to PlanStatus) {
s.ChangeStatus(m, int(from), int(to), m.ActionKey()+"_time", m.Time())
}
type PlanStatus int
const (
PlanCreate PlanStatus = iota
PlanProcess
PlanFinish
)
var PlanStatusList = map[PlanStatus]string{
PlanCreate: "create",
PlanProcess: "process",
PlanFinish: "finish",
}
func (s PlanStatus) String() string { return PlanStatusList[s] }

View File

@ -1,9 +1,9 @@
Volcanos(chat.ONIMPORT, { Volcanos(chat.ONIMPORT, {
_init: function(can, msg) { _init: function(can, msg) {
can.onimport.myView(can, msg, function(value) { return [ can.onimport.myView(can, msg, function(value) { return [
{view: html.TITLE, list: [value.title||value.name]}, {view: html.TITLE, list: [value.title, can.onimport.textView(can, value)]},
{view: html.STATUS, list: [value.uid.slice(0, 6), can.onimport.timeView(can, value), value.user_name]}, {view: html.STATUS, list: [can.onimport.beginTime(can, value), can.onimport.unitView(can, value, "task_count", "个")]},
{view: html.OUTPUT, list: [value.content||value.info]}, {view: html.OUTPUT, list: [value.content]}, can.onimport.titleAction(can, value),
] }) ] })
}, },
}) })

View File

@ -1,8 +1,11 @@
package xingmuguanli package xiangmuguanli
import ( import (
"shylinux.com/x/ice"
"shylinux.com/x/community/src/gonganxitong" "shylinux.com/x/community/src/gonganxitong"
"shylinux.com/x/enterprise/src/guanlixitong" "shylinux.com/x/enterprise/src/guanlixitong"
"shylinux.com/x/enterprise/src/xiangmuguanli/model"
) )
type Portal struct { type Portal struct {
@ -10,4 +13,11 @@ type Portal struct {
placeCreate string `name:"placeCreate city_name* company_name* project_name* project_type:select" role:"void"` placeCreate string `name:"placeCreate city_name* company_name* project_name* project_type:select" role:"void"`
} }
func (s Portal) AfterPlaceAuth(m *ice.Message, arg ...string) {
defer s.DashboardCreate(m, "")()
s.DashboardInsert(m, 1, "任务总量", "个", Task{}, "")
s.DashboardInsert(m, 2, "任务待办", "个", Task{}, "", "project_uid = ? AND status != ? AND status != ? AND status != ?", m.Option(model.PROJECT_UID), TaskRejected, TaskFinish, TaskCancel)
s.DashboardInsert(m, 3, "提报待办", "个", Done{}, "", "project_uid = ? AND status != ?", m.Option(model.PROJECT_UID), DoneFinish)
s.DashboardInsert(m, 4, "提报总量", "个", Done{}, "")
}
func init() { gonganxitong.PortalCmd(Portal{Portal: guanlixitong.NewPortal(userProject{}, project{})}) } func init() { gonganxitong.PortalCmd(Portal{Portal: guanlixitong.NewPortal(userProject{}, project{})}) }

View File

@ -1,7 +1,15 @@
{ {
"portal": "项目管理", "portal": "项目管理",
"plan": "项目计划", "warn": "进度监管", "todo": "待办事项", "task": "任务发布", "done": "任务提报", "plan": "项目计划", "warn": "进度监管", "todo": "待办事项", "task": "任务发布", "done": "任务提报",
"date": "会议安排",
"taskCreate": "创建任务", "taskList": "任务列表",
"doneCreate": "任务提报", "doneList": "提报列表",
"reback": "返工",
"style": {
"taskCreate": "notice"
},
"icons": { "icons": {
"date": "plan.png",
"plan": "plan.png", "plan": "plan.png",
"warn": "warn.png", "warn": "warn.png",
"todo": "todo.png", "todo": "todo.png",
@ -9,10 +17,15 @@
"done": "done.png" "done": "done.png"
}, },
"input": { "input": {
"My Project": "我的场景", "My Project": "我的项目",
"user_project_role": "成员角色", "user_project_role": "成员角色",
"project_name": "场景名称", "project_name": "项目名称",
"project_type": "场景类型" "plan_title": "计划名称",
"task_status": "任务状态",
"task_count": "任务数量",
"done_status": "提报状态",
"done_count": "提报数量",
"project_type": "项目类型"
}, },
"value": { "value": {
"user_project_role": { "user_project_role": {
@ -30,6 +43,39 @@
"RD": "研发群", "RD": "研发群",
"OP": "运维群", "OP": "运维群",
"HR": "人力群" "HR": "人力群"
},
"plan_status": {
"create": "待开始",
"process": "进行中",
"finish": "已完成",
"style": {
"create": "danger"
}
},
"task_status": {
"create": "待审批",
"rejected": "已驳回",
"approved": "待开始",
"process": "进行中",
"submit": "待验收",
"reback": "已返工",
"finish": "已完成",
"cancel": "已取消",
"style": {
"create": "danger",
"rejected": "danger",
"approved": "danger",
"submit": "danger",
"cancel": "danger"
}
},
"done_status": {
"create": "待开始",
"process": "进行中",
"finish": "已完成",
"style": {
"create": "danger"
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
package xingmuguanli package xiangmuguanli
import "shylinux.com/x/ice" import "shylinux.com/x/ice"

View File

@ -1,15 +1,139 @@
package xingmuguanli package xiangmuguanli
import "shylinux.com/x/ice" import (
"shylinux.com/x/ice"
kit "shylinux.com/x/toolkits"
type task struct { "shylinux.com/x/enterprise/src/xiangmuguanli/model"
)
type Task struct {
Table Table
order string `data:"4"` order string `data:"2"`
fields string `data:"title,content,user_uid"` fields string `data:"title,content,task_status,done_count,begin_time,end_time,process_time,finish_time,plan_uid,user_uid"`
create string `name:"create title* content*" role:"leader"` create string `name:"create plan_uid* title* content* begin_time:select@date end_time:select@date" role:"worker"`
remove string `name:"remove" role:"leader"` modify string `name:"modify title* content* begin_time:select@date end_time:select@date" role:"worker"`
remove string `name:"remove" role:"worker"`
reject string `name:"reject" role:"leader"`
approve string `name:"approve" role:"leader"`
process string `name:"process" role:"worker"`
submit string `name:"submit" role:"worker"`
reback string `name:"reback" role:"leader"`
finish string `name:"finish" role:"leader"`
cancel string `name:"cancel" role:"leader"`
doneCreate string `name:"doneCreate title* content* begin_time:select@date end_time:select@date" role:"worker"`
} }
func (s task) List(m *ice.Message, arg ...string) { s.ValueList(m, arg) } func (s Task) Create(m *ice.Message, arg ...string) {
s.ValueCreate(m, kit.ArgDef(arg, kit.Simple(model.BEGIN_TIME, m.Time(), model.END_TIME, m.Time("72h"))...)...)
s.SendMessage(m, "", "")
s.planCount(m).DashboardUpdate(m)
}
func (s Task) Remove(m *ice.Message, arg ...string) {
s.ValueRemove(m, arg...)
s.planCount(m).DashboardUpdate(m)
}
func (s Task) List(m *ice.Message, arg ...string) {
if !s.IsWorker(m) {
s.ApplyCheck(m, arg...)
return
}
user_uid := m.Option(model.USER_UID)
isLeader := s.IsLeader(m)
isWorker := s.IsWorker(m)
s.Orders(m, model.STATUS, model.DONE_COUNT, s.Desc(model.CREATED_AT))
s.ValueList(m, arg).Table(func(value ice.Maps) {
button := []ice.Any{}
defer func() { m.PushButton(button...) }()
switch TaskStatus(kit.Int(value[model.TASK_STATUS])) {
case TaskCreate:
if isLeader {
button = append(button, s.Approve, s.Reject)
}
if value[model.USER_UID] == user_uid {
button = append(button, s.Modify, s.Remove)
}
case TaskApproved:
if value[model.USER_UID] == user_uid {
button = append(button, s.Process)
}
case TaskProcess:
if isWorker {
button = append(button, s.DoneCreate)
}
if value[model.USER_UID] == user_uid {
button = append(button, s.Submit)
}
case TaskSubmit:
if isLeader {
button = append(button, s.Finish, s.Reback, s.Cancel)
}
case TaskFinish:
kit.If(m.FieldsIsDetail(), func() { s.DoneMessage(m) })
}
}).Display("")
s.SelectJoinPlan(m)
m.Option("otherList", "doneList")
}
func (s Task) Reject(m *ice.Message, arg ...string) {
s.ChangeStatus(m, int(TaskCreate), int(TaskRejected))
}
func (s Task) Approve(m *ice.Message, arg ...string) {
s.ChangeStatus(m, int(TaskCreate), int(TaskApproved))
}
func (s Task) Process(m *ice.Message, arg ...string) {
s.changeStatus(m, TaskApproved, TaskProcess)
}
func (s Task) Submit(m *ice.Message, arg ...string) {
if s.finishCheck(m, Done{}, "提报") {
return
}
s.ChangeStatus(m, int(TaskProcess), int(TaskSubmit))
}
func (s Task) Reback(m *ice.Message, arg ...string) {
s.ChangeStatus(m, int(TaskSubmit), int(TaskProcess))
}
func (s Task) Finish(m *ice.Message, arg ...string) {
s.changeStatus(m, TaskSubmit, TaskFinish)
}
func (s Task) Cancel(m *ice.Message, arg ...string) {
s.ChangeStatus(m, int(TaskSubmit), int(TaskCancel))
}
func (s Task) DoneCreate(m *ice.Message, arg ...string) {
s.OtherCreate(m, Done{}, append(arg, m.OptionSimple(model.PLAN_UID)...)...)
}
func (s Task) DoneList(m *ice.Message, arg ...string) {
s.OtherList(m, Done{}).Display("task.js")
}
func init() { ice.TeamCtxCmd(task{}) } func init() { ice.TeamCtxCmd(Task{}) }
func (s Task) changeStatus(m *ice.Message, from, to TaskStatus) {
s.ChangeStatus(m, int(from), int(to), m.ActionKey()+"_time", m.Time())
}
type TaskStatus int
const (
TaskCreate TaskStatus = iota
TaskRejected
TaskApproved
TaskProcess
TaskSubmit
TaskReback
TaskFinish
TaskCancel
)
var TaskStatusList = map[TaskStatus]string{
TaskCreate: "create",
TaskRejected: "rejected",
TaskApproved: "approved",
TaskProcess: "process",
TaskSubmit: "submit",
TaskReback: "reback",
TaskFinish: "finish",
TaskCancel: "cancel",
}
func (s TaskStatus) String() string { return TaskStatusList[s] }

View File

@ -1,15 +1,17 @@
package xingmuguanli package xiangmuguanli
import "shylinux.com/x/ice" import "shylinux.com/x/ice"
type todo struct { type todo struct {
Table Table
order string `data:"3"` order string `data:"4"`
fields string `data:"title,content,user_uid"` fields string `data:"title,content,user_uid"`
create string `name:"create title* content*" role:"leader"` create string `name:"create title* content*" role:"leader"`
remove string `name:"remove" role:"leader"` remove string `name:"remove" role:"leader"`
} }
func (s todo) List(m *ice.Message, arg ...string) { s.ValueList(m, arg) } func (s todo) List(m *ice.Message, arg ...string) {
s.ValueList(m, arg)
}
func init() { ice.TeamCtxCmd(todo{}) } func init() { ice.TeamCtxCmd(todo{}) }

View File

@ -1,4 +1,4 @@
package xingmuguanli package xiangmuguanli
import ( import (
"shylinux.com/x/ice" "shylinux.com/x/ice"

View File

@ -1,10 +1,10 @@
package xingmuguanli package xiangmuguanli
import "shylinux.com/x/ice" import "shylinux.com/x/ice"
type warn struct { type warn struct {
Tables Tables
order string `data:"2"` order string `data:"5"`
fields string `data:"title,content,user_uid"` fields string `data:"title,content,user_uid"`
create string `name:"create title* content*" role:"leader"` create string `name:"create title* content*" role:"leader"`
remove string `name:"remove" role:"leader"` remove string `name:"remove" role:"leader"`